SwiftUI 视图中修饰符的顺序影响视图外观

Order of modifiers in SwiftUI view impacts view appearance

我正在关注 Apple 系列中的 first tutorial,解释如何在 SwiftUI 应用程序中创建和组合视图。
在教程第6节第8步中,我们需要插入如下代码:

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

产生以下 UI:

现在,我注意到将代码中修饰符的顺序切换为以下方式时:

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

...Hello World 标签和地图之间有额外的 space。

问题

为什么修饰符的顺序在这里很重要,我怎么知道它何时重要?

将这些修饰符视为转换视图的函数。来自该教程:

To customize a SwiftUI view, you call methods called modifiers. 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.

顺序很重要是有道理的。

下面的结果会是什么?

  1. 取sheet张纸
  2. 在边缘周围绘制边框
  3. 剪出一个圆圈

对战:

  1. 取sheet张纸
  2. 剪出一个圆圈
  3. 在边缘周围绘制边框

文字墙传入

最好不要将修饰符视为修饰 MapView。相反,将 MapView().edgesIgnoringSafeArea(.top) 视为返回一个 SafeAreaIgnoringView,其 bodyMapView,并且根据其自身的顶边是否位于顶边而对其主体进行不同布局的安全区域。你应该这样想,因为这就是它的实际作用。

你怎么确定我说的是真的?将此代码放入您的 application(_:didFinishLaunchingWithOptions:) 方法中:

let mapView = MapView()
let safeAreaIgnoringView = mapView.edgesIgnoringSafeArea(.top)
let framedView = safeAreaIgnoringView.frame(height: 300)
print("framedView = \(framedView)")

现在option-clickmapView查看其推断类型,即普通MapView

接下来,option-clicksafeAreaIgnoringView看它的推断类型。它的类型是 _ModifiedContent<MapView, _SafeAreaIgnoringLayout>_ModifiedContent 是 SwiftUI 的实现细节,当它的第一个泛型参数(名为 Content)符合 View 时,它符合 View。在这种情况下,它的ContentMapView,所以这个_ModifiedContent也是一个View

接下来,option-clickframedView看它的推断类型。它的类型是 _ModifiedContent<_ModifiedContent<MapView, _SafeAreaIgnoringLayout>, _FrameLayout>.

所以你可以看到,在类型层面,framedView是一个视图,其内容的类型是safeAreaIgnoringView,而safeAreaIgnoringView是一个视图,其内容的类型是mapView.

但那些只是类型,类型的嵌套结构可能不会在 运行 时间在实际数据中表示,对吗? 运行 应用程序(在模拟器或设备上)并查看打印语句的输出:

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

我重新格式化了输出,因为 Swift 将它打印在一行上,这使得它很难理解。

反正我们可以看到其实framedViewapparently有一个content属性的值是safeAreaIgnoringView的类型,而object 有自己的 content 属性 其值为 MapView.

因此,当您将“修饰符”应用于 View 时,您并没有真正修改视图。您正在创建一个 new View,其 body/content 是原始 View.


既然我们了解了修饰符的作用(它们构造了包装器 Views),我们可以合理猜测这两个修饰符(edgesIgnoringSafeAreasframe)如何影响布局.

在某个时候,SwiftUI 遍历树以计算每个视图的框架。它以屏幕的安全区域作为我们 top-level ContentView 的框架开始。然后它访问 ContentView 的主体,即(在第一个教程中)VStack。对于 VStack,SwiftUI 将 VStack 的帧划分到堆栈的 children 中,其中三个 _ModifiedContent 紧随其后一个Spacer。 SwiftUI 查看 children 来计算分配给每个 space 的数量。第一个 _ModifiedChild(最终包含 MapView)有一个 _FrameLayout 修饰符,其 height 是 300 点,所以这就是 VStack 的高度被分配给第一个 _ModifiedChild.

最终 SwiftUI 找出 VStack 框架的哪一部分分配给每个 children。然后它访问每个 children 以分配它们的帧并布置 children 的 children。因此它使用 _FrameLayout 修饰符访问 _ModifiedContent,将其框架设置为与安全区域的顶部边缘相交且高度为 300 点的矩形。

由于视图是一个 _ModifiedContent,带有 _FrameLayout 修饰符,其 height 为 300,SwiftUI 检查分配的高度是否可以接受修饰符。是的,所以 SwiftUI 不必进一步更改框架。

然后访问那个_ModifiedContent的child,到达修饰符为`_SafeAreaIgnoringLayout的_ModifiedContent。它将 safe-area-ignoring 视图的框架设置为与 parent (frame-setting) 视图相同的框架。

接下来 SwiftUI 需要计算 safe-area-ignoring 视图的 child(MapView)的框架。默认情况下,child 获得与 parent 相同的帧。但是由于这个 parent 是一个 _ModifiedContent,其修饰符是 _SafeAreaIgnoringLayout,SwiftUI 知道它可能需要调整 child 的框架。由于修饰符的 edges 设置为 .top,SwiftUI 将 parent 框架的上边缘与安全区域的上边缘进行比较。在这种情况下,它们重合,因此 Swift 扩展 child 的框架以覆盖安全区域顶部上方的屏幕范围。因此 child 的框架延伸到 parent 的框架之外。

然后 SwiftUI 访问 MapView 并为其分配上面计算的帧,该帧超出安全区域到达屏幕边缘。因此 MapView 的高度是 300 加上超出安全区域顶部边缘的范围。

让我们通过在 safe-area-ignoring 视图周围绘制红色边框并在 frame-setting 视图周围绘制蓝​​色边框来检查这一点:

MapView()
    .edgesIgnoringSafeArea(.top)
    .border(Color.red, width: 2)
    .frame(height: 300)
    .border(Color.blue, width: 1)

屏幕截图显示,确实,两个 _ModifiedContent 视图的框架重合并且没有延伸到安全区域之外。 (您可能需要放大内容才能看到两个边框。)


这就是 SwiftUI 使用教程项目中的代码的方式。现在,如果我们按照您的建议交换 MapView 上的修饰符呢?

什么时候SwiftUI 访问 ContentViewVStack child,它需要将 VStack 的垂直范围分配给堆栈的 children,就像前面的例子一样。

这次,第一个 _ModifiedContent 是带有 _SafeAreaIgnoringLayout 修饰符的那个。 SwiftUI 发现它没有特定的高度,所以它查看 _ModifiedContent 的 child,现在是 _ModifiedContent _FrameLayout修饰符。此视图的固定高度为 300 点,因此 SwiftUI 现在知道 safe-area-ignoring _ModifiedContent 应该是 300 点高。因此 SwiftUI 将 VStack 范围的前 300 点授予堆栈的第一个 child(safe-area-ignoring _ModifiedContent)。

稍后,SwiftUI 访问第一个 child 以分配其实际框架并布置其 children。所以 SwiftUI 将 safe-area-ignoring _ModifiedContent 的框架设置为安全区域的前 300 个点。

接下来SwiftUI需要计算safe-area-ignoring_ModifiedContent的child的帧,也就是frame-setting_ModifiedContent。通常 child 获得与 parent 相同的帧。但是由于 parent 是 _ModifiedContent 修饰符 _SafeAreaIgnoringLayoutedges.top,SwiftUI 比较顶部parent 框架的边缘到安全区域的上边缘。在此示例中,它们重合,因此 SwiftUI 将 child 的框架扩展到屏幕的顶部边缘。因此,框架是 300 点加上安全区域顶部上方的范围。

当SwiftUI去设置child的框架时,它看到child是一个_ModifiedContent,修饰符是_FrameLayoutheight为300,由于框架高300多点,不兼容修改器,所以SwiftUI被迫调整框架。它将帧高度更改为 300,但 它最终的帧与 parent 不同。额外范围(安全区域外)已添加到框架的顶部,但更改框架的高度会修改框架的底部边缘。

因此最终效果是框架移动,而不是扩展到安全区域以上的范围。 frame-setting _ModifiedContent 得到一个覆盖屏幕前 300 点的框架,而不是安全区域的前 300 点。

SwiftUI然后访问frame-setting视图的child,也就是MapView,给它同样的frame.

我们可以使用相同的 border-drawing 技术进行检查:

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

这里我们可以看到safe-area-ignoring _ModifiedContent(这次有蓝色边框)和原来的代码有相同的框架:它从安全区域的顶部开始。但是我们也可以看到,现在 frame-setting _ModifiedContent 的边框(这次是红色边框)开始于屏幕的上边缘,而不是安全区域的上边缘,而底部框架的边缘也向上移动了相同的程度。

是的。确实如此。在 SwiftUI Essentials session 中,Apple 尝试尽可能简单地解释这一点。

更改顺序后-