Open suragch opened 4 months ago
@suragch to help us answer that question, can you investigate a little more deeply on modifiers like fill()
, stroke()
, and shadow()
?
Probably the most important detail that differentiates a property from a widget is how generally applicable a modifier is meant to be. For example, I don't know that it's even possible for us to implement a generic Stroke
widget - a stroke will require knowledge of a given shape to stroke. So that, perhaps, should be a widget property. But we should do that analysis for each item, both how its used in Swift UI, and what kind of limitations we might run into with widget composition.
@matthew-carroll I started out by taking a list of Swift property methods and going through them one by one to see if they should be a widget property or a composable widget. After while I discovered that some of these were extensions on Shape
while others were extensions on View
. My conclusion is that the Shape
extensions should be widget properties while the View
extensions have the potential to be composable widgets themselves.
Rather than completely rewrite my original answer, I'll keep the examples, and then move on to what belongs to Shape
and View
.
fill()
This fills the shape with a color or gradient.
HStack {
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
}
fill
makes sense for a shape, but it is ambiguous in the generic context. For example, filling Text
would imply setting the text color, but filling a VStack
might imply setting the background color. I recommend fill()
should be a property of the shape.
stroke()
This modifier applies a stroke to the shape with a specified color and line width.
HStack {
Rectangle()
.stroke(Color.red, lineWidth: 4)
.frame(width: 100, height: 100)
Circle()
.stroke(Color.green, lineWidth: 2)
.frame(width: 100, height: 100)
}
stroke()
doesn't have a clear meaning when applied to other types of views like Text
, Image
, or VStack
. The concept of stroking is closely tied to the geometry of a shape. I recommend implementing stroke()
as a widget property.
strokeBorder()
This property is similar to stroke, but the stroke is drawn inside the shape's border, preserving the shape's size.
HStack {
Rectangle()
.stroke(Color.blue, lineWidth: 10)
.frame(width: 100, height: 100)
Rectangle()
.strokeBorder(Color.blue, lineWidth: 10)
.frame(width: 100, height: 100)
}
For the same reason as stroke
this should be a widget property.
trim(from:to:)
This modifier trims a shape by specifying a start and end point, effectively creating a partial shape. The from and to parameters are values between 0 and 1, representing the percentage of the shape to be trimmed.
This modifier is not applicable to other types of views like Text
, Image
, or VStack
because they don't have the concept of a path or a shape that can be trimmed. So trim should be a widget property.
inset(by:)
This modifier insets the content of a shape by a specified amount on all sides.
HStack {
Rectangle()
.inset(by: 10)
.fill(Color.blue)
.frame(width: 100, height: 100)
Circle()
.inset(by: 20)
.fill(Color.red)
.frame(width: 100, height: 100)
}
inset
only applies to insetable shapes, not views like Text
. For that reason, it should be a property on the shape.
Question: Are we going to follow in some way Swift protocols like Shape
and InsetableShape
? Would we use mixins?
scale(x:y:anchor:)
This modifier scales a shape by the specified scale factors along the x-axis and y-axis. The anchor
parameter determines the point around which the scaling occurs.
HStack {
Rectangle()
.scale(x: 1.5, y: 0.5, anchor: .center)
.fill(Color.blue)
.frame(width: 100, height: 100)
Circle()
.scale(x: 0.8, y: 0.8, anchor: .topLeading)
.fill(Color.red)
.frame(width: 100, height: 100)
}
Only shapes have a scale
method. If you want to scale other views, you need to use scaleEffect
. So, scale
should be a property of a shape.
rotation(_:anchor:)
and rotationEffect(_:anchor:)
rotation
rotates a shape around and anchor while rotationEffect
rotates any view around an anchor. I'm not sure why shape needs its own method.
HStack {
Rectangle()
.rotation(Angle(degrees: 32))
.fill(Color.blue)
.frame(width: 100, height: 100)
Text("Hello")
.rotationEffect(Angle(degrees: 73))
}
rotation
could be a shape widget property while rotationEffect
could be a composable widget.
Anything that belongs to Shape
should be a property, not a widget itself.
Property methods that are defines as extensions to View
could be their own composable widgets:
Here are a few methods that belong to View
and thus also apply to shapes:
padding(_:)
frame(width:height:alignment:)
transformEffect()
shadow(color:radius:x:y:)
blendMode(_:)
compositingGroup()
mask(_:)
clipShape(_:)
opacity(_:)
border(_:)
aspectRatio(_:contentMode:)
background(_:)
alignmentGuide(_:computeValue:)
gesture(_:)
overlay(_:)
animation(_:)
Those could probably all be widgets.
There is an offset(x:y:)
in both Shape and View. I'm not sure why that is. I suppose it could be a widget and a property, though this seems like it is duplicating the logic. More study needed on this one.
Thanks for looking into it. That rationale makes sense to me. Feel free to start with whatever piece you'd like.
I'm implementing the a
Rectangle
widget (#26).Here is what a Rectangle view in SwiftUI can do:
The swift code for that looks like so:
In that code you can see a number of different modifier methods:
fill
frame
stroke
cornerRadius
shadow
One of the guiding principles is that
swift_ui
doesn't use modifier methods.How to avoid modifier methods gives a hypothetical alternative for Flutter:
Is this how we want to define the API, by creating widgets for all of the following?
Fill
Frame
Stroke
CornerRadius
Shadow
Not widget properties?
@matthew-carroll