11

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

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

这会产生以下 UI:

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

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

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

问题

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

4

3 回答 3

27

传入的文字墙

最好不要将修饰符视为修改MapView. 相反,可以考虑MapView().edgesIgnoringSafeArea(.top)返回一个SafeAreaIgnoringViewwho bodyis the MapView,并且根据它自己的顶部边缘是否在安全区域的顶部边缘,它会以不同的方式布局其主体。你应该这样想,因为它实际上就是这样做的。

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

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

现在选项单击mapView以查看其推断类型,即 plain MapView

接下来,单击选项safeAreaIgnoringView以查看其推断类型。它的类型是_ModifiedContent<MapView, _SafeAreaIgnoringLayout>. _ModifiedContent是 SwiftUI 的一个实现细节,View当它的第一个泛型参数(名为Content)符合View. 在这种情况下,它ContentMapView,所以这_ModifiedContent也是一个View

接下来,单击选项framedView以查看其推断类型。它的类型是_ModifiedContent<_ModifiedContent<MapView, _SafeAreaIgnoringLayout>, _FrameLayout>.

所以你可以看到,在类型级别,是framedView一个内容类型为的视图,一个内容类型为 的视图。safeAreaIgnoringViewsafeAreaIgnoringViewmapView

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

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 将它打印在一行上,这使得它很难理解。

无论如何,我们可以看到实际上framedView显然有一个content属性,其值为 的类型safeAreaIgnoringView,而该对象有它自己的content属性,其值为 a MapView

因此,当您对 a 应用“修饰符”时View,您并没有真正修改视图。您正在创建一个 Viewbody/content是原始的View.


现在我们了解了修饰符的作用(它们构造了 wrapper ),我们可以对这两个修饰符(和)如何影响布局View做出合理的猜测。edgesIgnoringSafeAreasframe

在某些时候,Sw​​iftUI 会遍历树来计算每个视图的帧。它以屏幕的安全区域作为我们顶层的框架开始ContentView。然后它访问ContentView的主体,即(在第一个教程中) a VStack。对于 a VStack,SwiftUI 将VStack堆栈的子元素的框架划分为三个_ModifiedContents 后跟 a Spacer。SwiftUI 会查看孩子们以确定要分配给每个孩子的空间。第一个_ModifiedChild(最终包含MapView)有一个300 点的_FrameLayout修饰符,所以这就是分配给第一个的高度height的多少。VStack_ModifiedChild

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

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

然后它访问那个的孩子_ModifiedContent,到达_ModifiedContent谁的修饰符是`_SafeAreaIgnoringLayout。它将忽略安全区域的视图的框架设置为与父(框架设置)视图相同的框架。

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

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

让我们通过在忽略安全区域的视图周围绘制红色边框和在框架设置视图周围绘制蓝​​色边框来检查这一点:

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

添加边框的原始教程代码的屏幕截图

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


这就是 SwiftUI 如何处理教程项目中的代码。现在,如果我们MapView按照您的建议交换周围的修饰符会怎样?

当 SwiftUI 访问 的VStack子级时ContentView,它需要VStack在堆栈的子级之间划分 ' 的垂直范围,就像在前面的示例中一样。

这一次,第一个_ModifiedContent是带有_SafeAreaIgnoringLayout修饰符的。SwiftUI 发现它没有特定的高度,所以它查看_ModifiedContent' 的孩子,现在是_ModifiedContent带有_FrameLayout修饰符的 。这个视图有 300 点的固定高度,因此 SwiftUI 现在知道忽略安全区域的_ModifiedContent高度应该是 300 点。因此 SwiftUI 将VStack' 范围的前 300 个点授予堆栈的第一个孩子(安全区域忽略_ModifiedContent)。

之后,SwiftUI 会访问第一个子节点以分配其实际框架并布置其子节点。因此 SwiftUI 将 safe-area-ignoring_ModifiedContent的框架设置为安全区域的前 300 个点。

接下来 SwiftUI 需要计算 safe-area-ignoring_ModifiedContent的孩子的框架,即 frame-setting _ModifiedContent。通常,孩子获得与父母相同的框架。但是由于父级是 a ,其_ModifiedContent修饰符为is ,因此 SwiftUI 会将父级框架的上边缘与安全区域的上边缘进行比较。在此示例中,它们重合,因此 SwiftUI 将子项的框架延伸到屏幕的顶部边缘。因此,框架是 300 点加上安全区域顶部上方的范围。_SafeAreaIgnoringLayoutedges.top

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

所以最终的效果是框架被移动,而不是扩展,超出了安全区域的范围。frame-setting_ModifiedContent得到一个覆盖屏幕前 300 个点的帧,而不是安全区域的前 300 个点。

SwiftUI 然后访问框架设置视图的子视图,即MapView,并为其提供相同的框架。

我们可以使用相同的边框绘制技术来检查这一点:

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

修改后的教程代码的屏幕截图,添加了边框

在这里我们可以看到,忽略安全区域_ModifiedContent(这次是蓝色边框)与原始代码中的框架相同:它从安全区域的顶部开始。但是我们也可以看到,现在frame-setting的frame _ModifiedContent(这次是红色边框)从屏幕的上边缘开始,而不是安全区的上边缘,而且frame的下边缘也已经上移了同样的幅度。

于 2019-06-05T06:21:12.347 回答
14

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

在此处输入图像描述

更改顺序后——

在此处输入图像描述

于 2019-06-06T01:49:33.573 回答
4

将这些修饰符视为转换视图的函数。从那个教程:

要自定义 SwiftUI 视图,您需要调用称为修饰符的方法。修饰符包裹视图以更改其显示或其他属性。每个修改器都会返回一个新视图,因此通常链接多个修改器,垂直堆叠。

顺序很重要是有道理的。

下面的结果会是什么?

  1. 拿一张纸
  2. 在边缘周围画一个边框
  3. 剪出一个圆圈

相对:

  1. 拿一张纸
  2. 剪出一个圆圈
  3. 在边缘周围画一个边框
于 2019-06-04T20:42:27.310 回答