SWIFTUI布局系统指南-第3部分

757 阅读10分钟

翻译地址

最初,SwiftUI的布局系统可能看起来有点不灵活,因为它的默认概念和API套件并没有给我们提供很多像素级的控制,而是专注于利用一组强大的平台定义的默认设置,从而使系统能够代表我们做出许多共同的布局决策。

但是,一旦我们从表面看,就会有大量不同的自定义选项和重写,我们可以应用这些选项和重写来调整SwiftUI布局系统及其默认行为集。因此,在本系列文章的第三部分(也是最后一部分)中,让我们探讨一些定制选项,以及它们如何让我们在定义SwiftUI布局时解决常见的冲突和消除歧义来源。

载不再使用的应用程序。

遭遇冲突

把我们最后停下来的地方捡起来。第二部分-在我们的事件视图中同时添加了页眉和页脚之后,现在让我们向它添加一些实际的内容。和以前一样,我们将坚持本文中的占位符内容,以便能够完全专注于探索SwiftUI布局系统本身。

让我们从创建一个新视图开始,该视图允许我们使用RoundedRectangle形状,放置在ZStack连同一个Text:

struct ImagePlaceholder: View {
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 10).stroke()
            Text("Image placeholder")
        }
    }
}

接下来,让我们添加上述实例ImagePlaceholder,以及一个描述文本,我们的主要ContentView-它现在将包含作为活动屏幕一部分显示的最后一组视图:

struct ContentView: View {
    var body: some View {
        VStack {
            EventHeader()
            ImagePlaceholder()
            Text("This is a description")
            Spacer()
            EventInfoList()
        }.padding()
    }
}

上面代码的结果(您可以显示使用PREVIEW(按钮)向我们展示了SwiftUI各种形状的一个非常有趣的方面--就像间隔一样,它们总是占据尽可能多的空间。因此,鉴于我们的描述文本目前非常短,我们的图像占位符最终延伸到了相当大的一部分屏幕。

现在,让我们看看,如果我们用更长的时间来改变这种情况--例如,重复我们使用了50次以上的文本,如下所示:

struct ContentView: View {
    var body: some View {
        VStack {
            EventHeader()
            ImagePlaceholder()
            Text(makeDescription())
            Spacer()
            EventInfoList()
        }.padding()
    }
}

private extension ContentView {
    func makeDescription() -> String {
        String(repeating: "This is a description ", count: 50)
    }
}

这就是事情开始发展的地方。真的很有趣。SwiftUI布局系统不仅截断了我们现在更长的文本,还截断了后面的文本EventInfoBadge在屏幕底部-所有的同时,仍然给我们很大一部分可用的空间ImagePlaceholder(具有讽刺意味的是,这种观点可以说是最适合在这种情况下调整大小的)。

这是怎么回事?这一切都取决于SwiftUI的基本布局规则(我们在其中查看了这些规则)。第一部分)工作--每个视图都负责确定自己的大小,而且只负责在那之后是每个父母决定如何在自己的框架内定位和适合自己的孩子。

因此,自从我们ImagePlaceholder我们的描述文本现在都要求的帧要比我们可以同时容纳的帧要大得多。VStack-布局系统被迫妥协,首先尽可能压缩每个视图(这就是导致我们EventInfoBadge),然后将可用空间平均分配给其子空间。

幸运的是,SwiftUI附带了许多工具,我们可以使用这些工具来解决上述类型的布局冲突--而不必自己手动绘制每个视图,或者通过逃逸进入UIKit或AppKit的土地。

布局优先级

让我们先来看看布局优先级,这使我们能够告诉SwiftUI布局系统,这些视图在尊重它们的首选大小方面是最重要的(或最不重要的)。每个视图的布局优先级为零,然后可以通过应用layoutPriority()改性剂。以下是我们如何做到这一点,从而使我们的描述具有更高的优先级:

struct ContentView: View {
    var body: some View {
        VStack {
            EventHeader()
            ImagePlaceholder()
            Text(makeDescription()).layoutPriority(1)
            Spacer()
            EventInfoList()
        }.padding()
    }
}

请注意,没有必要走极端并使用布局优先级值,如999或.infinity-任何大于零的数值都将对我们的布局产生影响。

上面的调整肯定会使我们的视图看起来更好(同样,您可以使用PREVIEW按钮来查看它当前的样子)--现在我们的描述获得了可用空间的更大一部分。然而,我们的拖后腿EventInfoBadge仍然是压扁,我们的图像占位符现在有一个更小的高度。

解决这个问题的方法之一EventInfoBadge问题是要做我们上面做的相反的事情,并且较低我们的图像占位符的布局优先级,而不是增加我们描述的那个-像这样:

struct ContentView: View {
    var body: some View {
        VStack {
            EventHeader()
            ImagePlaceholder().layoutPriority(-1)
            Text(makeDescription())
            Spacer()
            EventInfoList()
        }.padding()
    }
}

这再次更好,但我们的图像占位符仍然缩小到它绝对最小的高度(等于它的文本的线条高度),这看起来不太好。为了解决这个问题,我们还可以使用.frame()修饰语:

struct ContentView: View {
    var body: some View {
        VStack {
            EventHeader()
            ImagePlaceholder()
                .layoutPriority(-1)
                .frame(minHeight: 100)
            Text(makeDescription())
            Spacer()
            EventInfoList()
        }.padding()
    }
}

我们的图像占位符现在看起来很棒,我们的描述文本也是如此-然而,我们的EventInfoBadge文本再次被截断。为了解决最后一个问题,让我们提高我们的布局优先级EventInfoList,告诉布局系统将其高度优先于所有其他事项:

struct ContentView: View {
    var body: some View {
        VStack {
            EventHeader()
            ImagePlaceholder()
                .layoutPriority(-1)
                .frame(minHeight: 100)
            Text(makeDescription())
            Spacer()
            EventInfoList().layoutPriority(1)
        }.padding()
    }
}

SwiftUI的布局优先级系统是一个简单而强大的工具,它使我们能够指定视图布局的明确顺序--它可以帮助我们解决冲突,比如如何调整视图的大小以适应可用的空间。

固定尺寸

然而,布局优先级的一个问题是,应用它们有时会感觉到“一颗鼹鼠”-在我们适用的每一次调整和修正中,都会出现一个新的问题。我们已经看到,这开始发生在上面,当我们必须提高和降低布局的优先次序,以回应各种问题。

因此,尽管调整视图的布局优先级可能是应用一次性修复的一个很好的方法,但值得庆幸的是,只工具,它允许我们调整SwiftUI的布局行为。这些工具中的另一个工具是fixedSize()修饰符(顾名思义)使我们能够固定视图的大小,以其首选的宽度或高度(或两者兼而有之)。

使用该修饰符,我们可以实现与前面示例完全相同的结果,这一次只需要引入额外的布局优先级(图像占位符除外)--方法是EventInfoList一种固定的垂直尺寸,防止它被压缩:

struct ContentView: View {
    var body: some View {
        VStack {
            EventHeader()
            ImagePlaceholder()
                .layoutPriority(-1)
                .frame(minHeight: 100)
            Text(makeDescription())
            Spacer()
            EventInfoList().fixedSize(horizontal: false, vertical: true)
        }.padding()
    }
}

以进一步说明fixedSize()修饰符有效,让我们看看w如果我们也给我们的EventInfoList固定的水平尺寸也是:

struct ContentView: View {
    var body: some View {
        VStack {
            EventHeader()
            ImagePlaceholder()
                .layoutPriority(-1)
                .frame(minHeight: 100)
            Text(makeDescription())
            Spacer()
            EventInfoList().fixedSize(horizontal: true, vertical: true)
        }.padding()
    }
}

如上面示例的预览所示,修复信息列表的宽度将导致整个ContentView被延伸到屏幕的界限之外,这可能一开始看起来很奇怪。

原因是,因为我们现在正在阻止布局系统调整EventInfoList,我们的根VStack将被迫伸展自己以占用同样大的宽度(因为堆栈总是调整自身大小以适应其子视图中的所有子视图)--这反过来又为我们的其他子视图提供了更多的水平空间,尽管该空间部分超出了范围。

自定义对齐指南

最后,让我们看一看如何使用自定义对齐指南,以及它们如何成为使用其他形式的对齐工具的一个很好的替代方法--例如填充物和偏移量。为此,我们将回到我们的经核实的徽章从…第一部分,作为快速提醒,我们最终将其实现为View扩展使用ZStack而.offset()修饰语:

extension View {
    func addVerifiedBadge(_ isVerified: Bool) -> some View {
        ZStack(alignment: .topTrailing) {
            self

            if isVerified {
                Image(systemName: "checkmark.circle.fill")
                    .offset(x: 3, y: -3)
            }
        }
    }
}

虽然上面的代码确实有效,但它确实对我们最终显示的徽章的大小做出了某些假设--因为我们的偏移量目前是硬编码到3x3点,而不管系统呈现的图像大小如何。

为了解决这个问题,让我们替换使用.offset()带有两个自定义对齐指南的修饰符。通过应用.alignmentGuide()对视图的修饰符,我们可以使用自定义计算闭包来调整它在使用给定的水平或垂直对齐时如何定位。

因为我们ZStack当前使用.topTrailing对齐,让我们使用这组排列来调整我们的徽章的位置,按照这两个指南放置它的中心-如下所示:

extension View {
    func addVerifiedBadge(_ isVerified: Bool) -> some View {
        ZStack(alignment: .topTrailing) {
            self

            if isVerified {
                Image(systemName: "checkmark.circle.fill")
                    .alignmentGuide(HorizontalAlignment.trailing) {
                        $0[HorizontalAlignment.center]
                    }
                    .alignmentGuide(VerticalAlignment.top) {
                        $0[VerticalAlignment.center]
                    }
            }
        }
    }
}

上面的结果看起来很好,但是不像我们之前使用硬编码度量集时所看到的那么好。本质上,我们希望将我们的徽章图像稍微偏移到日历图标本身的中心,以使它更有感觉。附它的主机视图。

为了实现这一点,不涉及任何固定的偏移值,让我们执行我们的对齐使用我们的徽章图像的宽度和高度的百分比,而不是使用它的中心。这很容易做到,因为ViewDimensions传递到每个自定义对齐指南闭包中的上下文还包含正在对齐的视图的宽度和高度:

extension View {
    func addVerifiedBadge(_ isVerified: Bool) -> some View {
        ZStack(alignment: .topTrailing) {
            self

            if isVerified {
                Image(systemName: "checkmark.circle.fill")
                    .alignmentGuide(HorizontalAlignment.trailing) {
                        $0.width * 0.8
                    }
                    .alignmentGuide(VerticalAlignment.top) {
                        // Here we first align our view's bottom edge
                        // according to its host view's top edge,
                        // and we then subtract 80% of its height.
                        $0[.bottom] - $0.height * 0.8
                    }
            }
        }
    }
}

这种方法与以前基于偏移量的方法之间的一个小区别是,在计算其主机视图的总体框架时,徽章现在将被包括在内,这在这种情况下并没有太大的区别,并且可以通过给予徽章负布局优先级来避免。

虽然定制的对齐指南非常强大,但它们是非常强大的。“重”在语法方面-因此,与其将上述修饰符保持为内联,不如将它们移到新的View可应用于我们希望作为徽章对齐的任何观点的扩展:

extension View {
    func alignAsBadge(withRatio ratio: CGFloat = 0.8,
                      alignment: Alignment = .topTrailing) -> some View {
        alignmentGuide(alignment.horizontal) {
            $0.width * ratio
        }
        .alignmentGuide(alignment.vertical) {
            $0[.bottom] - $0.height * ratio
        }
    }
}

有了上面的扩展,我们现在可以大大简化我们的经过验证的徽章实现,而不是如下所示:

extension View {
    func addVerifiedBadge(_ isVerified: Bool) -> some View {
        ZStack(alignment: .topTrailing) {
            self

            if isVerified {
                Image(systemName: "checkmark.circle.fill")
                    .alignAsBadge()
            }
        }
    }
}

所以.alignmentGuide()修饰符使我们能够覆盖和调整视图是如何在水平或垂直对齐的情况下对齐的--这在构建完全自定义布局或调整单个视图的位置时都是非常有用的。还有一个API可以让我们定义完全定制对齐(通过实现AlignmentID),我们可以在以后的文章中对此进行更深入的研究。

载不再使用的应用程序。

结语

我们现在已经完成了关于SwiftUI布局系统的三部分指南。我希望您喜欢它,并希望它为您提供了关于SwiftUI布局系统如何工作的新见解,以及可用于自定义其行为的各种API和工具。

虽然我的目标肯定是使这个指南尽可能的彻底,但它当然没有涵盖SWIFUI布局系统的许多不同方面--所以我确信我们会重新讨论这个主题,也许会更早,而不是更晚。

但是现在,让我们再一次回顾一下本系列的第三部分也就是最后一部分中的内容:

  • 调整布局优先级可以很好地根据每个视图的首选大小来调整其优先级。
  • 应用固定框架尺寸对于视图,可以防止其水平或垂直地调整大小(或两者兼而有之)。
  • 自定义对齐指南允许我们在使用给定对齐时调整视图的定位方式,当我们希望将一个视图相对于另一个视图定位时,这将是非常有用的。

推荐👇:

如果你想一起进阶,不妨添加一下交流群1012951431

面试题资料或者相关学习资料都在群文件中 进群即可下载!