How to Access Modifiers of a View in Swiftui

How can I access modifiers of a view in SwiftUI?

The return type of myImage is:

_ModifiedContent<Image, _ShadowEffect>

We can access the original image by doing:

myImage.content

We can access the shadow effect modifier by typing:

myImage.modifier

So to do what you want, you have to type:

print(myImage.modifier.radius)

In SwiftUI, How to access the view that is calling onDisappear() modifier in the closure of it?

Since your HostView is a simple struct you can just hold the EditView as a var inside your HostView like this:

struct HostView: View {
var editView = EditView()
var body: some View {
editView
.onDisappear {
print(editView.property1)
}
}
}

Modify view from within a view modifier

Here is possible solution:

if to you have declaration like

struct HighlightedText: View {
var defaultFount = Font.body
var highlightedFont = Font.headline

// ... other code

then your custom modifiers could be as

extension HighlightedText {
func defaultFont(_ font: Font) -> Self {
var view = self
view.defaultFount = font
return view
}

func highlightedFont(_ font: Font) -> Self {
var view = self
view.highlightedFont = font
return view
}
}

How to access to the children views in SwiftUI?

This is what you're looking for, if I'm understanding your question correctly:

struct FullButton<Label>: View where Label: View {
var action: () -> Void
var label: () -> Label

var body: some View {
Button(action: self.action, label: self.label)
}
}

This would allow you to pass whatever content you want to be displayed on your button, meaning that the code you have here would now work:

FullButton(action: {
print("touched")
}) {
Text("Button")
}

Update

After looking over your question several times, I've realized that your confusion is stemming from a misunderstanding of what is happening when you create a normal Button.

In the code below, I'm creating a Button. The button takes two arguments - action and label.

Button(action: {}, label: {
Text("Button")
})

If we look at the documentation for Button, we see that it is declared like this:

struct Button<Label> where Label : View

If we then look at the initializers, we see this:

init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)

Both action and label expect closures. action expects a closure with a return type of Void, and label expects a @ViewBuilder closure with a return type of Label. As defined in the declaration for Button, Label is a generic representing a View, so really, label is expecting a closure that returns a View.

This is not unique to Button. Take HStack, for example:

struct HStack<Content> where Content : View

init(alignment: VerticalAlignment = .center, spacing: Length? = nil, @ViewBuilder content: () -> Content)

Content serves the same purpose here that Label does in Button.

Something else to note - when we create a button like this...

Button(action: {}) {
Text("Button")
}

...we're actually doing the same thing as this:

Button(action: {}, label: {
Text("Button")
})

In Swift, when the last argument in a method call is a closure, we can omit the argument label and append the closure to the outside of the closing parenthesis.

In SwiftUI, you cannot implicitly pass content to any View. The View must explicitly accept a @ViewBuilder closure in its initializer.

And so, you cannot pass a @ViewBuilder closure to FullButton unless FullButton accepts a @ViewBuilder closure as an argument in its initializer, as shown at the beginning of my answer.

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 Views), 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 _ModifiedContents 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)

screen shot of original tutorial code with added borders

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)
}

screen shot of modified tutorial code with added borders

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.



Related Topics



Leave a reply



Submit