SwiftUI 布局简介
在这个技术项目中,我们将探讨SwiftUI如何处理布局。有些事情已经解释过了,有些可能是你自己弄明白的,但更多的是你在这一点上想当然的事情,所以我希望一个详细的探索能真正为SwiftUI的工作方式提供一些启示。
在此过程中,您还将学习如何创建更高级的布局对齐,使用GeometryReader
构建特殊效果,以及更多——我知道您会热衷于在自己的应用程序中部署的一些真正强大的功能。
继续使用单视图应用程序模板创建一个新的iOS项目,并将其命名为 layoutDageMetricy。您需要在资源目录中提供一个图像,以便遵循有关自定义对齐指南的章节,但它可以是任何您想要的——它实际上只是一个占位符。
SwiftUI 中布局的工作原理
所有的 SwiftUI 布局都有三个简单的步骤,理解这些步骤是每次获得优秀布局的关键。步骤如下:
- 父视图提供一个大小并询问其子视图的大小。
- 子视图根据自己的信息,它会选择自己的尺寸,而父视图必须尊重这个选择。
- 然后父视图在其坐标空间中定位子视图。
在幕后,SwiftUI执行第四步:尽管它将位置和大小存储为浮点数,但在渲染时,SwiftUI会将所有像素舍入到最接近的值,这样我们的图形仍然清晰。
这三条规则看起来很简单,但它们允许我们创建非常复杂的布局,每个视图都可以决定如何以及何时调整大小,而无需父级参与。
为了演示这些规则的实际操作,我希望您修改默认的SwiftUI模板以添加background()
修饰符,如下所示:
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.background(Color.red)
}
}
你会看到背景色紧紧围绕着文本本身——它只占用足够的空间来适应我们提供的内容。
现在,想想这个问题:ContentView
有多大?如您所见,ContentView
的主体(它呈现的内容)是一些带有背景色的文本。所以ContentView
的大小总是和它的主体大小一样,不多不少。这被称为 布局中立 (layout neutral):ContentView
本身没有任何大小,而是可以根据需要进行调整以适应任何大小。
在Project3 为什么SwiftUI的修饰符顺序很重要?中,我向您解释过,当您对视图应用修饰符时,我们实际上会得到一个名为ModifiedContent
的新视图类型,它存储了原始视图及其修饰符。这意味着当我们应用修饰符时,进入层次结构的实际视图是修改后的视图,而不是原始视图。
在我们的简单background()
示例中,这意味着ContentView
中的顶层视图是背景,而内部是文本。背景和ContentView
一样是布局中立的,因此它只会根据需要传递布局信息——您可以最终得到一系列布局信息,直到最终得到确定的答案。
如果我们把这个放到三步布局系统中,我们最终会有一个类似这样的对话:
- SwiftUI:“嘿,ContentView,你自己拥有整个屏幕——你需要多少?“(父视图询问大小)
- ContentView:“我不在乎;我是布局中立的。让我问我的孩子:嘿,背景,你可以使用整个屏幕——你需要多少?“(父父视图询问大小)
- 背景:“我也不在乎;我的布局也是中性的。让我问我的孩子:嘿,Text,你可以把整个屏幕留给你自己——你需要多少?“(父视图询问大小)
- Text:“嗯,我的文本是默认字体的‘Hello,World’,所以我需要X像素宽Y像素高。我不需要整个屏幕,只需要这个。”(孩子选择它的大小。)
- 背景:“明白了。嘿,ContentView:我需要X * Y像素。”
- ContentView:“了解。嘿,SwiftUI:我需要X * Y像素。”
- SwiftUI:“好的。那么,这会留下很多空间,所以我会把你的尺寸放在中间。”(父视图在其坐标空间中定位子视图。)
所以,当我们说Text("Hello, World!").background(Color.red)
,文本视图成为其背景的子视图。当涉及到视图及其修改器时,SwiftUI有效地从下到上工作。
现在考虑一下这个布局:
Text("Hello, World!")
.padding(20)
.background(Color.red)
这一次对话更为复杂:padding()
不再为其子级提供所有空间,因为它需要从每边减去20点,以确保有足够的空间填充。然后,当答案从文本视图返回时,padding()
根据请求在每侧添加20个点来填充它。
所以,更像这样:
- SwiftUI:ContentView,你可以拥有整个屏幕,你需要多少?
- ContentView:背景,你可以有整个屏幕,你需要多少?
- 背景:填充, 你可以有整个屏幕,你需要多少?
- 填充:文本,你可以拥有整个屏幕每边减20点之后的大小,你需要多少?
- 文本:我需要X * Y。
- 填充:我需要X * Y加上每边20个点。
- 背景:我需要X * Y加上每边20个点。
- ContentView:我需要X * Y加上每边20个点。
- SwiftUI:好的,我把你放在中间。
如果你还记得 为什么SwiftUI的修饰符顺序很重要?。也就是说,这个代码:
Text("Hello, World!")
.padding()
.background(Color.red)
和这个:
Text("Hello, World!")
.background(Color.red)
.padding()
产生两种不同的结果。希望现在您可以理解为什么:background()
是布局无关的,所以它通过询问子对象需要多少空间并使用相同的值来确定需要多少空间。如果background()
的子级是文本视图,那么背景将非常适合文本,但是如果子级是padding()
,那么它将接收回调整后的值,包括填充量。
这些布局规则带来了两个有趣的副作用。
首先,如果视图层次结构完全是布局中立的,那么它将自动占用所有可用空间。例如,形状和颜色是与布局无关的,因此,如果视图包含颜色而没有其他内容,它将自动填充屏幕,如下所示:
var body: some View {
Color.red
}
记住,Color.red
本身就是一个视图,但由于它是布局中立的,所以可以以任何大小绘制。当我们在background()
中使用它时,简化的布局对话是这样工作的:
- 背景:嘿,文本,你可以有整个屏幕,你想要多少?
- 文本:我需要X乘Y点;我不需要其余的。
- 背景:好的。嘿,红色,你可以有X乘Y点,你想要多少?
- 红色:我不在乎;我的布局是中性的,所以X乘Y点听起来不错。
第二个有趣的副作用是我们前面遇到的:如果我们在一个不能调整大小的图像上使用frame()
,我们会得到一个更大的Frame,而图像内部没有改变大小。这在以前可能会令人困惑,但一旦将 Frame 视为图像的父对象,这就完全有意义了:
- ContentView 提供了整个屏幕。
- frame 报告它想要300x300。
- 然后 frame 会询问里面的图像它想要什么尺寸。
- 不可调整大小的图像返回固定大小例如:64x64。
- 然后 frame 将图像定位在其自身的中心。
当你听苹果公司的SwiftUI工程师谈论修饰符时,你会听到他们把它们称为视图——“the frame view”、“ the background view”等等。我认为这是一个很好的心理模型,有助于准确地理解到底发生了什么:应用修饰符创建新的视图,而不仅仅是修改现有的视图。
译自
Layout and geometry: Introduction
How layout works in SwiftUI