Order of modifiers in SwiftUI view impacts view appearance
Wall of text incoming
It is better not to think of the modifiers as modifying the MapView
. Instead, think of MapView().edgesIgnoringSafeArea(.top)
as returning a SafeAreaIgnoringView
whose body
is the MapView
, and which lays out its body differently depending on whether its own top edge is at the top edge of the safe area. You should think of it that way because that is what it actually does.
How can you be sure I'm telling the truth? Drop this code into your application(_:didFinishLaunchingWithOptions:)
method:
let mapView = MapView()
let safeAreaIgnoringView = mapView.edgesIgnoringSafeArea(.top)
let framedView = safeAreaIgnoringView.frame(height: 300)
print("framedView = \(framedView)")
Now option-click mapView
to see its inferred type, which is plain MapView
.
Next, option-click safeAreaIgnoringView
to see its inferred type. Its type is _ModifiedContent<MapView, _SafeAreaIgnoringLayout>
. _ModifiedContent
is an implementation detail of SwiftUI and it conforms to View
when its first generic parameter (named Content
) conforms to View
. In this case, its Content
is MapView
, so this _ModifiedContent
is also a View
.
Next, option-click framedView
to see its inferred type. Its type is _ModifiedContent<_ModifiedContent<MapView, _SafeAreaIgnoringLayout>, _FrameLayout>
.
So you can see that, at the type level, framedView
is a view whose content has the type of safeAreaIgnoringView
, and safeAreaIgnoringView
is a view whose content has the type of mapView
.
But those are just types, and the nested structure of the types might not be represented at run time in the actual data, right? Run the app (on a simulator or a device) and look at the output of the print statement:
framedView =
_ModifiedContent<
_ModifiedContent<
MapView,
_SafeAreaIgnoringLayout
>,
_FrameLayout
>(
content:
SwiftUI._ModifiedContent<
Landmarks.MapView,
SwiftUI._SafeAreaIgnoringLayout
>(
content: Landmarks.MapView(),
modifier: SwiftUI._SafeAreaIgnoringLayout(
edges: SwiftUI.Edge.Set(rawValue: 1)
)
),
modifier:
SwiftUI._FrameLayout(
width: nil,
height: Optional(300.0),
alignment: SwiftUI.Alignment(
horizontal: SwiftUI.HorizontalAlignment(
key: SwiftUI.AlignmentKey(bits: 4484726064)
),
vertical: SwiftUI.VerticalAlignment(
key: SwiftUI.AlignmentKey(bits: 4484726041)
)
)
)
)
I've reformatted the output because Swift prints it on a single line, which makes it very hard to understand.
Anyway, we can see that in fact framedView
apparently has a content
property whose value is the type of safeAreaIgnoringView
, and that object has its own content
property whose value is a MapView
.
So, when you apply a “modifier” to a View
, you're not really modifying the view. You're creating a new View
whose body
/content
is the original View
.
Now that we understand what modifiers do (they construct wrapper View
s), we can make a reasonable guess about how these two modifiers (edgesIgnoringSafeAreas
and frame
) affect layout.
At some point, SwiftUI traverses the tree to compute each view's frame. It starts with the screen's safe area as the frame of our top-level ContentView
. It then visits the ContentView
's body, which is (in the first tutorial) a VStack
. For a VStack
, SwiftUI divides up the frame of the VStack
among the stack's children, which are three _ModifiedContent
s followed by a Spacer
. SwiftUI looks through the children to figure out how much space to allot to each. The first _ModifiedChild
(which ultimately contains the MapView
) has a _FrameLayout
modifier whose height
is 300 points, so that's how much of the VStack
's height gets assigned to the first _ModifiedChild
.
Eventually SwiftUI figures out which part of the VStack
's frame to assign to each of the children. Then it visits each of the children to assign their frames and lay out the children's children. So it visits that _ModifiedContent
with the _FrameLayout
modifier, setting its frame to a rectangle that meets the top edge of the safe area and has a height of 300 points.
Since the view is a _ModifiedContent
with a _FrameLayout
modifier whose height
is 300, SwiftUI checks that the assigned height is acceptable to the modifier. It is, so SwiftUI doesn't have to change the frame further.
Then it visits the child of that _ModifiedContent
, arriving at the _ModifiedContent
whose modifier is `_SafeAreaIgnoringLayout. It sets the frame of the safe-area-ignoring view to the same frame as the parent (frame-setting) view.
Next SwiftUI needs to compute the frame of the safe-area-ignoring view's child (the MapView
). By default, the child gets the same frame as the parent. But since this parent is a _ModifiedContent
whose modifier is _SafeAreaIgnoringLayout
, SwiftUI knows it might need to adjust the child's frame. Since the modifier's edges
is set to .top
, SwiftUI compares the top edge of the parent's frame to the top edge of the safe area. In this case, they coincide, so Swift expands the frame of the child to cover the extent of the screen above the top of the safe area. Thus the child's frame extends outside of the parent's frame.
Then SwiftUI visits the MapView
and assigns it the frame computed above, which extends beyond the safe area to the edge of the screen. Thus the MapView
's height is 300 plus the extent beyond the top edge of the safe area.
Let's check this by drawing a red border around the safe-area-ignoring view, and a blue border around the frame-setting view:
MapView()
.edgesIgnoringSafeArea(.top)
.border(Color.red, width: 2)
.frame(height: 300)
.border(Color.blue, width: 1)
The screen shot reveals that, indeed, the frames of the two _ModifiedContent
views coincide and don't extend outside the safe area. (You might need to zoom in on the content to see both borders.)
That's how SwiftUI works with the code in the tutorial project. Now what if we swap the modifiers on the MapView
around as you proposed?
When SwiftUI visits the VStack
child of the ContentView
, it needs to divvy up the VStack
's vertical extent amongst the stack's children, just like in the prior example.
This time, the first _ModifiedContent
is the one with the _SafeAreaIgnoringLayout
modifier. SwiftUI sees that it doesn't have a specific height, so it looks to the _ModifiedContent
's child, which is now the _ModifiedContent
with the _FrameLayout
modifier. This view has a fixed height of 300 points, so SwiftUI now knows that the safe-area-ignoring _ModifiedContent
should be 300 points high. So SwiftUI grants the top 300 points of the VStack
's extent to the stack's first child (the safe-area-ignoring _ModifiedContent
).
Later, SwiftUI visits that first child to assign its actual frame and lay out its children. So SwiftUI sets the safe-area-ignoring _ModifiedContent
's frame to exactly the top 300 points of the safe area.
Next SwiftUI needs to compute the frame of the safe-area-ignoring _ModifiedContent
's child, which is the frame-setting _ModifiedContent
. Normally the child gets the same frame as the parent. But since the parent is a _ModifiedContent
with a modifier of _SafeAreaIgnoringLayout
whose edges
is .top
, SwiftUI compares the top edge of the parent's frame to the top edge of the safe area. In this example, they coincide, so SwiftUI extends the frame of the child to the top edge of the screen. The frame is thus 300 points plus the extent above the top of the safe area.
When SwiftUI goes to set the frame of the child, it sees that the child is a _ModifiedContent
with a modifier of _FrameLayout
whose height
is 300. Since the frame is more than 300 points high, it isn't compatible with the modifier, so SwiftUI is forced to adjust the frame. It changes the frame height to 300, but it does not end up with the same frame as the parent. The extra extent (outside the safe area) was added to the top of the frame, but changing the frame's height modifies the bottom edge of the frame.
So the final effect is that the frame is moved, rather than expanded, by the extent above the safe area. The frame-setting _ModifiedContent
gets a frame that covers the top 300 points of the screen, rather than the top 300 points of the safe area.
SwiftUI then visits the child of the frame-setting view, which is the MapView
, and gives it the same frame.
We can check this using the same border-drawing technique:
if false {
// Original tutorial modifier order
MapView()
.edgesIgnoringSafeArea(.top)
.border(Color.red, width: 2)
.frame(height: 300)
.border(Color.blue, width: 1)
} else {
// LinusGeffarth's reversed modifier order
MapView()
.frame(height: 300)
.border(Color.red, width: 2)
.edgesIgnoringSafeArea(.top)
.border(Color.blue, width: 1)
}
Here we can see that the safe-area-ignoring _ModifiedContent
(with the blue border this time) has the same frame as in the original code: it starts at the top of the safe area. But we can also see that now the frame of the frame-setting _ModifiedContent
(with the red border this time) starts at the top edge of the screen, not the top edge of the safe area, and the bottom edge of the frame has also been shifted up by the same extent.
Why does modifier order matter in SwiftUI?
A modifier is function that returns some View
by wrapping the content View it modifies. For example .padding
returns some View
that implements a padding functionality and its content is the View it modifies, plus the padding:
So, conceptually:
HStack {
Text("Hello world")
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(20.0)
}
is roughly equivalent to (names and params are made up):
HStack(content: {
CornerRadiusView(20.0, content: {
BackgroundView(Color(UIColor.darkGray), content: {
PaddingView(content: {
Text("Hello world")
})
})
})
})
Hopefully, it's clear from the example how the order of the modifiers changes the view hierarchy.
SwiftUI: More than 4 combined Text Views not working with view modifiers
You can create one var for text and then use it in Group.
struct DialogueBox: View {
var body: some View {
GroupBox {
Group {
Text("this").foregroundColor(.pink)
+ Text(" code ")
+ Text("works")
+ Text(" well ").foregroundColor(.green)
}.font(.footnote)
Group {
textView
}.font(.footnote)
}
}
private var textView: Text {
Text("but").foregroundColor(.pink)
+ Text(" this ")
+ Text("code")
+ Text(" does ").foregroundColor(.green)
+ Text(" not ").foregroundColor(.blue)
+ Text(" for some reason ").foregroundColor(.red)
+ Text(" not ").foregroundColor(.blue)
+ Text(" for some reason ").foregroundColor(.red)
+ Text(" not ").foregroundColor(.blue)
+ Text(" for some reason ").foregroundColor(.red)
+ Text(" not ").foregroundColor(.blue)
+ Text(" for some reason ").foregroundColor(.red)
}
}
How to separate out view modifiers into callable/includable structs
You may try the following:
class UserSettings: ObservableObject {
@Published var mapType: String = "Satellite"
var fgColor: Color {
switch mapType {
case "Satellite", "Hybrid", "AssassinsCreed", "Aubergine", "Dark", "MidnightCommand", "Night":
return Color(UIColor.white)
default:
return Color(UIColor.black)
}
}
}
struct ColorTheme: View {
@ObservedObject var userSettings = UserSettings()
var body: some View {
Text("test")
.foregroundColor(userSettings.fgColor)
}
}
You may also consider making mapType
an enum.
EDIT
You may be looking for a View extension:
extension View {
@ViewBuilder
func customFgColor(mapType: String) -> some View {
switch mapType {
case "Satellite", "Hybrid", "AssassinsCreed", "Aubergine", "Dark", "MidnightCommand", "Night":
return self.foregroundColor(Color(UIColor.white))
default:
return self.foregroundColor(Color(UIColor.black))
}
}
}
struct ColorTheme: View {
@ObservedObject var userSettings = UserSettings()
var body: some View {
Text("test")
.customFgColor(mapType: userSettings.mapType)
}
}
Related Topics
Select() from Linq in Swift 3.0
Urlsession.Shared.Datataskpublisher Not Working on iOS 13.3
Difference Between 2 Dates in Weeks and Days Using Swift 3 and Xcode 8
Simple Way to Read Local File Using Swift
Lazy Initialisation and Retain Cycle
Uiapplication.Shared.Delegate Equivalent for Scenedelegate Xcode11
Why Is There No Universal Base Class in Swift
How to Stop Timer in Text View
Navigationview Pops Back to Root, Omitting Intermediate View
How to Make the Scroll of a Tableview Inside Scrollview Behave Naturally
Multiple Type Constraints in Swift
Dynamic Row Hight Containing Texteditor Inside a List in Swiftui
Intrinsiccontentsize() - Method Does Not Override Any Method from Its Superclass
Determining If Swift Dictionary Contains Key and Obtaining Any of Its Values
How to Document the Parameters of a Function'S Closure Parameter in Swift 3