除了声明式 DSL 和强大的数据绑定外,SwiftUI 还具有全新的布局系统,该系统在许多方面结合了手动框架计算的明确性和自动布局的适应性。乍一看可能看起来很简单的系统,但是一旦我们开始将其各种构建块组合成越来越复杂的布局,它就会提供巨大的灵活性和强大的功能。
frame
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
}
}
默认情况下,SwiftUI 允许每个视图根据其渲染的容器选择自己的大小,然后居中放置在其父视图中。所以上面代码的结果是在屏幕中央呈现一个小图标——而不是像我们基于 UIKit 和 AppKit 的工作方式所预期的那样位于左上角或左下角。
接下来,让我们把图标放大一点,比如 50x50 pt。关于如何实现这一点的初步想法可能是使用.frame()
告诉我们的视图采用该大小,如下所示:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.frame(width: 50, height: 50)
}
}
然而,虽然上面的代码会产生一个50x50 pt的视图,但我们图标的大小将与之前完全一样——这乍一看可能有点奇怪。为了探究为什么会这样,让我们给我们的视图一个背景颜色,这样我们就可以很容易地看到它在屏幕上的frame:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.frame(width: 50, height: 50)
.background(Color.red)
}
}
有了上面的内容,我们可以看到我们的视图确实是正确的大小 。只是我们的图标似乎完全不受我们的.frame()
修饰符的影响。将修饰符应用于视图时,我们通常根本不是修饰视图,而是将其封装在一个新的、透明的视图中。因此,在上面调用.background()
时,我们实际上是将该background modifier应用于包装Image的新视图上面,而不是Image本身。
所以,从布局的角度来看,我们的Image没有任何变化,它仍然在其父视图中居中,只是这次它的父视图是一个新的 50x50 透明包装视图,但渲染结果看起来是一样的。
在SwiftUI中 视图负责确定它们自己的大小,我们需要告诉我们的Image调整自身大小以占用所有可用空间,而不是坚持其默认大小。为了实现这一点,我们只需使用.resizable()
修饰符:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.background(Color.red)
}
}
padding
接下来,我们来看看SwiftUI 中的padding。就像在其他布局系统中一样,如 CSS,padding 使我们能够在其自己的frame内偏移视图的内容。然而,我们在view modifiers中使用padding
的位置,可以获得完全不同的渲染结果。例如,让我们通过在上述代码的末尾附加.padding()
修饰符来应用paddding
:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.background(Color.red)
.padding()
}
}
上面的结果可能不是我们所期望的,因为我们给了calendar图标外边距。 没有背景颜色的额外空白。想一下,这与我们之前在应用.frame()
修饰符时遇到的情况完全相同。调用.padding()
实际上并没有改变我们之前的视图和修饰符,它只是在前面表达式的结果周围添加了空白而已。
事实上,如果我们在调用.padding()
之后添加第二个.background()
修饰符,就会更加清晰的展示这种情况。因为第二个背景颜色将在padding内渲染出来:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.background(Color.red)
.padding()
.background(Color.blue)
}
}
因此,如果我们希望添加内边距inner padding,我们需要在添加背景之前使用padding()
修饰符——就像这样:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.padding()
.background(Color.red)
}
}
每个修饰符modifier本质上都将它调用的视图包装在另一个视图中,如果我们在应用.frame()
修饰符之前调用.padding()
,我们的图标将缩小,因为padding将应用在我们固定的50x50
容器中——迫使我们调整图像尺寸以适应较小的尺寸:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.padding()
.frame(width: 50, height: 50)
.background(Color.red)
}
}
为了完成我们的calendar图标视图,让我们对其应用cornerRadius
并使其前景色为白色——最后将所有代码提取到一个名为CalendarView
的新视图中,如下所示:
struct CalendarView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.padding()
.background(Color.red)
.cornerRadius(10)
.foregroundColor(.white)
}
}
一般而言,每当我们完成一个独立的功能组件 UI 部分时,将代码提取到新View
实现中通常是个好主意,以避免出现臃肿视图。
Stacks and spacers
SwiftUI 中的各种stacks和spacers乍一看可能非常简单和有限,实际上却可以用来表达几乎无限的布局组合:
struct ContentView: View {
var body: some View {
VStack {
CalendarView()
}
}
}
上述内容VStack
实际上根本不会影响我们的布局,因为 在SwiftUI中stacks不会拉伸自己以占据其父级 。相反,它们只是根据其子级的总大小调整自己的大小。
要移动我们的CalendarView
,我们还必须将Spacer
添加到我们的stacks中。在HStack
或者VStack
中放置spacers时,spacers总是占据尽可能多的空间,在下面代码中,渲染后我们的CalendarView
会被推到屏幕顶部:
struct ContentView: View {
var body: some View {
VStack {
CalendarView()
Spacer()
}
}
}
stacks的酷炫之处在于它们可以通过嵌套来构建日益复杂的布局,而无需任何形式的手动frame计算。
struct ContentView: View {
var body: some View {
HStack {
VStack {
CalendarView()
Spacer()
}
Spacer()
}.padding()
}
}
接下来,让我们向视图中添加一个Text
,以模拟calendar事件的一组详细信息。由于在本文中我们将坚持仅探索 SwiftUI 的布局系统,因此我们现在将硬编码我们的内容Text
:
struct ContentView: View {
var body: some View {
HStack {
VStack {
CalendarView()
Spacer()
}
Text("Event title").font(.title)
Spacer()
}.padding()
}
}
看看上面的代码,我们希望Text
出现在CalendarView
水平轴的右边。
struct ContentView: View {
var body: some View {
HStack(alignment: .top) {
VStack {
CalendarView()
Spacer()
}
Text("Event title").font(.title)
Spacer()
}.padding()
}
}
下面我们继续在文字视图下面添加一个用来描述的文字视图:
struct ContentView: View {
var body: some View {
HStack(alignment: .top) {
VStack {
CalendarView()
Spacer()
}
VStack(alignment: .leading) {
Text("Event title").font(.title)
Text("Location")
}
Spacer()
}.padding()
}
}
然而,虽然上述布局有效,但可以被简化,以便更容易阅读代码。
因此,将ContentView
的body
代码提取到一个专用组件中,同时对其进行重构并命名为EventHeader
:
struct EventHeader: View {
var body: some View {
HStack(spacing: 15) {
CalendarView()
VStack(alignment: .leading) {
Text("Event title").font(.title)
Text("Location")
}
Spacer()
}
}
}
回到我们的ContentView
,我们现在可以将它的主体变成一个VStack
包含我们的新EventHeader
组件以及Spacer
,使我们的布局代码更容易理解:
struct ContentView: View {
var body: some View {
VStack {
EventHeader()
Spacer()
}.padding()
}
}
只要有可能,最好不断地将ContenView
中的代码提取到专用组件中。以这种方式编码通常可以让我们自然地将我们的 UI 分成原子部分,而无需我们预先进行大量的架构设计工作。
ZStacks 和 offset
最后,让我们快速了解一下 SwiftUI 的ZStack
类型,它使我们能够使用从后到前的顺序,在深度方面堆叠一系列视图。
举个例子,假设在之前的calendar视图顶部添加显示一个小的“验证徽章”的支持。在 top-trailing
放置一个checkmark
图标。为了以更通用的方式实现它,让我们对View
进行扩展,将所有视图包装在ZStack
内(它本身不会影响视图的布局),它还可以选择包含我们的checkmark
图标 :
extension View {
func addVerifiedBadge(_ isVerified: Bool) -> some View {
ZStack(alignment: .topTrailing) {
self
if isVerified {
Image(systemName: "checkmark.circle.fill")
.offset(x: 3, y: -3)
}
}
}
}
请注意ZStack
如何在alignment
上为我们提供对其的完整二维控制,我们可以使用它来将我们的图标定位在父视图的top-trailing
。然后我们还将.offset()
modifier应用到我们的徽章上,这会将它稍微移动到其父视图的边界之外。
有了上述内容,我们现在可以有条件地将我们的新徽章添加到我们CalendarView
上,在eventIsVerified
属性设置为true
时可以显示出来:
struct CalendarView: View {
var eventIsVerified = true
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.padding()
.background(Color.red)
.cornerRadius(10)
.foregroundColor(.white)
.addVerifiedBadge(eventIsVerified)
}
}
将ZStack
与.offset()
修饰符一起使用是向视图添加各种叠加层的好方法,而根本不会影响该视图自己的布局。我们可以使用该技术来实现进度加载、应用内通知以及我们希望在现有视图层次结构之上呈现的许多其他类型的视图。
结论
让我们总结一下到目前为止我们所涵盖的内容:
- SwiftUI 的核心布局引擎的工作原理是要求每个子视图根据其父视图的边界确定自己的大小,然后要求每个父视图将其子视图定位在自己的边界内。
- 视图修饰符View modifiers通常将当前视图包装在另一个视图中,这就是为什么我们可以根据调用修饰符的顺序获得完全不同的布局结果。
- 使用
.frame()
和.padding()
修饰符可以让我们调整视图的大小和内部边距,只要该视图配置为相应地调整自身大小。 - 使用
HStack
,VStack
和ZStack
我们可以一起在水平方向,垂直或深度方向排列视图。 - 使用
offset()
我们可以移动视图而不影响其周围环境,这在实现叠加和其他类型的视图重叠时非常有用。