简体   繁体   English

SwiftUI视图中的修改器顺序会影响视图外观

[英]Order of modifiers in SwiftUI view impacts view appearance

I'm following the first tutorial in Apple's series explaining how to create & combine views in a SwiftUI application. 我正在关注如何在SwiftUI应用程序中创建和组合视图的Apple系列中的第一个教程
In step 8 of section 6 in the tutorial, we need to insert the following code: 在本教程的第6节的第8步中,我们需要插入以下代码:

MapView()
    .edgesIgnoringSafeArea(.top)
    .frame(height: 300)

which produces the following UI: 它产生以下UI:

Now, I noticed that when switching the order of the modifiers in the code to the following way: 现在,我注意到在将代码中修饰符的顺序切换为以下方式时:

MapView()
    .frame(height: 300) // height set first
    .edgesIgnoringSafeArea(.top)

...there's extra space between the Hello World label and the map. ... Hello World标签和地图之间有额外的空间。

Question

Why is the order of the modifiers important here, and how do I know when it is important? 为什么修饰符的顺序在这里很重要,我怎么知道它何时重要?

Wall of text incoming 传入的文本墙

It is better not to think of the modifiers as modifying the MapView . 最好不要将修饰符视为修改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. 相反,将MapView().edgesIgnoringSafeArea(.top)视为返回其bodyMapViewSafeAreaIgnoringView ,并根据其自身的上边缘是否位于安全区域的顶部边缘而不同地放置其主体。 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: 将此代码放入您的application(_:didFinishLaunchingWithOptions:)方法:

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 . 现在,选择 - 单击mapView以查看其推断类型,即普通MapView

Next, option-click safeAreaIgnoringView to see its inferred type. 接下来,选择 - 单击safeAreaIgnoringView以查看其推断类型。 Its type is _ModifiedContent<MapView, _SafeAreaIgnoringLayout> . 它的类型是_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 . _ModifiedContent_ModifiedContent的一个实现细节,当它的第一个通用参数(名为Content )符合View时,它符合View In this case, its Content is MapView , so this _ModifiedContent is also a View . 在这种情况下,其ContentMapView ,因此_ModifiedContent也是一个View

Next, option-click framedView to see its inferred type. 接下来,选择 - 单击framedView以查看其推断类型。 Its type is _ModifiedContent<_ModifiedContent<MapView, _SafeAreaIgnoringLayout>, _FrameLayout> . 它的类型是_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 . 因此,您可以看到,在类型级别, framedView是一个视图,其内容的类型为safeAreaIgnoringView ,而safeAreaIgnoringView是一个视图,其内容的类型为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: 运行应用程序(在模拟器或设备上)并查看print语句的输出:

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. 我已经重新格式化了输出,因为Swift将它打印在一行上,这使得它很难理解。

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 . 无论如何,我们可以看到实际上framedView显然有一个content属性,其值是safeAreaIgnoringView的类型,并且该对象有自己的content属性,其值为MapView

So, when you apply a “modifier” to a View , you're not really modifying the view. 因此,当您将“修改器”应用于View ,您实际上并未修改视图。 You're creating a new View whose body / content is the original View . 您正在创建一个新的 Viewbody / content是原始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. 现在我们了解了修饰符的作用(它们构造了包装器View ),我们可以合理地猜测这两个修饰符( edgesIgnoringSafeAreasframe )如何影响布局。

At some point, SwiftUI traverses the tree to compute each view's frame. 在某些时候,Sw​​iftUI遍历树以计算每个视图的帧。 It starts with the screen's safe area as the frame of our top-level ContentView . 它从屏幕的安全区域开始,作为我们顶级ContentView的框架。 It then visits the ContentView 's body, which is (in the first tutorial) a VStack . 然后它访问ContentView的主体,这是(在第一个教程中)一个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 . 对于VStack ,SwiftUI划分了的框架VStack堆栈的孩子,这是三者之中_ModifiedContent秒,然后一个Spacer SwiftUI looks through the children to figure out how much space to allot to each. SwiftUI通过子视图来查找分配给每个子节点的空间。 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 . 第一个_ModifiedChild (最终包含MapView )有一个_FrameLayout修饰符,其height为300点,因此VStack的高度被分配给第一个_ModifiedChild

Eventually SwiftUI figures out which part of the VStack 's frame to assign to each of the children. 最终SwiftUI计算出VStack框架的哪一部分分配给每个孩子。 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. 因此,它使用_FrameLayout修改器访问_ModifiedContent ,将其框架设置为符合安全区域顶边的矩形,高度为300 _FrameLayout

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. 由于视图是带有_FrameLayout修饰符的_ModifiedContent ,其height为300,因此SwiftUI会检查修改器是否接受指定的高度。 It is, so SwiftUI doesn't have to change the frame further. 它是,所以SwiftUI不必进一步改变框架。

Then it visits the child of that _ModifiedContent , arriving at the _ModifiedContent whose modifier is `_SafeAreaIgnoringLayout. 然后它访问_ModifiedContent的子_ModifiedContent ,到达_ModifiedContent其修饰符为`_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 ). 接下来,SwiftUI需要计算安全区域忽略视图的子框架( 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. 但由于此父级是_ModifiedContent其修饰符为_SafeAreaIgnoringLayout ,因此SwiftUI知道可能需要调整子级的帧。 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. 由于改性剂的edges被设定为.top ,SwiftUI父框架的顶边进行比较,以安全区的顶部边缘。 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. 在这种情况下,它们重合,因此Swift 扩展了孩子的框架,以覆盖安全区域顶部上方的屏幕范围。 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. 然后SwiftUI访问MapView并为其分配上面计算的帧,该帧超出安全区域到屏幕边缘。 Thus the MapView 's height is 300 plus the extent beyond the top edge of the safe area. 因此, MapView的高度为300加上超出安全区域顶部边缘的范围。

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. 实际上,屏幕截图显示两个_ModifiedContent视图的帧重合并且不会延伸到安全区域之外。 (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. 这就是SwiftUI如何使用教程项目中的代码。 Now what if we swap the modifiers on the MapView around as you proposed? 现在,如果我们按照您的建议交换MapView上的修改器怎么办?

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. 当SwiftUI访问ContentViewVStackVStack ,它需要在栈的子VStack之间分配VStack的垂直范围,就像前面的例子一样。

This time, the first _ModifiedContent is the one with the _SafeAreaIgnoringLayout modifier. 这次,第一个_ModifiedContent是具有_SafeAreaIgnoringLayout修饰符的_SafeAreaIgnoringLayout 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. SwiftUI发现它并没有一个特定的高度,所以看起来在_ModifiedContent的孩子,也就是现在的_ModifiedContent_FrameLayout修改。 This view has a fixed height of 300 points, so SwiftUI now knows that the safe-area-ignoring _ModifiedContent should be 300 points high. 这个视图有300个固定高度,所以SwiftUI现在知道安全区域忽略_ModifiedContent应该是300点高。 So SwiftUI grants the top 300 points of the VStack 's extent to the stack's first child (the safe-area-ignoring _ModifiedContent ). 所以SwiftUI授予前300点的VStack的程度堆栈的第一个孩子(安全区-忽略_ModifiedContent )。

Later, SwiftUI visits that first child to assign its actual frame and lay out its children. 之后,SwiftUI访问第一个孩子来分配其实际框架并布置其子项。 So SwiftUI sets the safe-area-ignoring _ModifiedContent 's frame to exactly the top 300 points of the safe area. 因此,SwiftUI将安全区域忽略_ModifiedContent的框架设置为安全区域的前300个点。

Next SwiftUI needs to compute the frame of the safe-area-ignoring _ModifiedContent 's child, which is the frame-setting _ModifiedContent . 接下来,SwiftUI需要计算安全区域忽略_ModifiedContent的子_ModifiedContent的框架,这是框架设置_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. 但由于父母是_ModifiedContent用的改性剂_SafeAreaIgnoringLayout ,其edges.top ,SwiftUI父框架的顶部边缘比较安全区的顶部边缘。 In this example, they coincide, so SwiftUI extends the frame of the child to the top edge of the screen. 在这个例子中,它们重合,因此SwiftUI将子框架扩展到屏幕的上边缘。 The frame is thus 300 points plus the extent above the top of the safe area. 因此,框架是300点加上安全区域顶部上方的范围。

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. 当SwiftUI去设置子框架时,它会看到子项是_ModifiedContent ,其修饰符为_FrameLayoutheight为300.由于框架高于300点,因此与修改器不兼容,因此SwiftUI被迫调整框架。 It changes the frame height to 300, but it does not end up with the same frame as the parent . 它将帧高度更改为300,但它不会以与父级相同的帧结束 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. 框架设置_ModifiedContent获取一个框架,覆盖屏幕的前300个点,而不是安全区域的前300个点。

SwiftUI then visits the child of the frame-setting view, which is the MapView , and gives it the same frame. 然后SwiftUI访问框架设置视图的子项,即MapView ,并为其提供相同的框架。

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. 在这里我们可以看到安全区域忽略_ModifiedContent (这次带有蓝色边框)与原始代码具有相同的框架:它从安全区域的顶部开始。 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. 但我们也可以看到,现在框架设置的框架_ModifiedContent (这次带红色边框)从屏幕的上边缘开始,而不是安全区域的上边缘,框架的下边缘也是在同样的程度上被提升了。

Yes. 是。 It does. 确实如此。 In the SwiftUI Essentials session, Apple tried explaining this as simple as possible. 在SwiftUI Essentials会话中,Apple尝试尽可能简单地解释这一点。

在此输入图像描述

After changing the order - 更改订单后 -

在此输入图像描述

Think of these modifiers as functions that transform the view. 将这些修饰符视为转换视图的函数。 From that tutorial: 从该教程:

To customize a SwiftUI view, you call methods called modifiers. 要自定义SwiftUI视图,请调用称为修饰符的方法。 Modifiers wrap a view to change its display or other properties. 修改器包装视图以更改其显示或其他属性。 Each modifier returns a new view, so it's common to chain multiple modifiers, stacked vertically. 每个修改器都返回一个新视图,因此链接垂直堆叠的多个修改器是很常见的。

It makes sense the that order matters. 这个顺序很重要。

What would the result of the following be? 以下结果是什么?

  1. Take sheet of paper 拿一张纸
  2. Draw a border around the edge 在边缘周围绘制边框
  3. Cut out a circle 剪出一个圆圈

Versus: 与:

  1. Take sheet of paper 拿一张纸
  2. Cut out a circle 剪出一个圆圈
  3. Draw a border around the edge 在边缘周围绘制边框

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM