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

Lin*_*rth 9 swift swiftui

我正在关注Apple系列文章中的第一篇教程,其中介绍了如何在SwiftUI应用程序中创建和合并视图。
在本教程第6节的第8步中,我们需要插入以下代码:

MapView()
    .edgesIgnoringSafeArea(.top)
    .frame(height: 300)
Run Code Online (Sandbox Code Playgroud)

生成以下UI:

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

MapView()
    .frame(height: 300) // height set first
    .edgesIgnoringSafeArea(.top)
Run Code Online (Sandbox Code Playgroud)

... Hello World标签和地图之间有多余的空间。

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

rob*_*off 21

文字墙传入

最好不要将修饰符视为对的修改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)")
Run Code Online (Sandbox Code Playgroud)

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

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

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

因此,您可以看到,在类型级别上,framedView是一个内容类型为的视图safeAreaIgnoringView,并且safeAreaIgnoringView是一个内容类型为的视图mapView

但是这些仅仅是类型,类型的嵌套结构可能不会在运行时在实际数据中表示出来,对吗?运行该应用程序(在模拟器或设备上),然后查看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)
                    )
                )
            )
    )
Run Code Online (Sandbox Code Playgroud)

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

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

因此,当您将修饰符应用于时View,您实际上并没有在修改视图。您正在创建一个新的 View,其body/ content是原来的View


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

在某个时候,Sw​​iftUI遍历树以计算每个视图的框架。它以屏幕的安全区域作为顶层框架开始ContentView。然后,它访问ContentView的主体(在第一个教程中)为VStack。对于a VStack,SwiftUI VStack在堆栈的子级中将的框架划分为3,_ModifiedContent后跟一个Spacer。SwiftUI会仔细查看这些子级,以算出每个子级有多少空间。第一个_ModifiedChild(最终包含MapView)具有一个300点的_FrameLayout修饰符height,因此这是将VStack'的高度分配给第一个的高度_ModifiedChild

最终,SwiftUI找出了VStack框架的哪一部分分配给每个孩子。然后,它会拜访每个孩子以分配他们的框架并布置孩子的孩子。因此,它_ModifiedContent使用_FrameLayout修改器进行访问,将其框架设置为与安全区域的上边缘相交且高度为300点的矩形。

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

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

接下来,SwiftUI需要计算忽略安全区域的视图的子框架(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)
Run Code Online (Sandbox Code Playgroud)

带有边框的原始教程代码的屏幕截图

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


这就是SwiftUI与教程项目中的代码一起工作的方式。现在,如果我们MapView按照您的建议在周围交换修饰符怎么办?

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

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

之后,SwiftUI访问第一个孩子,以分配其实际框架并布置其孩子。因此,SwiftUI将忽略安全区域_ModifiedContent的框架设置为恰好位于安全区域的前300个点。

接下来,SwiftUI需要计算忽略安全区域_ModifiedContent的孩子的框架,即frame-setting _ModifiedContent。通常,孩子会得到与父母相同的框架。但由于父母是_ModifiedContent用的改性剂_SafeAreaIgnoringLayout,其edges.top,SwiftUI比较父框架到安全区域的顶部边缘的顶部边缘。在此示例中,它们重合,因此SwiftUI将子框架扩展到屏幕的顶部边缘。因此,框架为300点加上安全区域顶部上方的范围。

当SwiftUI去设定孩子的框架,它看到的孩子是_ModifiedContent与的改性剂_FrameLayout,其height是300由于框架是高度超过300点,这是不与改性剂兼容,因此SwiftUI被迫调整框架。它将框架高度更改为300,但最终不会以与父框架相同的框架结束。多余的范围(安全区域之外)已添加到框架的顶部,但是更改框架的高度会修改框架的底部边缘。

因此,最终效果是框架移动了而不是扩展了安全区域上方的范围。框架设置_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)
}
Run Code Online (Sandbox Code Playgroud)

带有边框的修改后的教程代码的屏幕截图

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


小智 9

是。是的 在SwiftUI Essentials会话中,Apple试图尽可能简单地解释这一点。

在此处输入图片说明

更改订单后-

在此处输入图片说明