SwiftUI为我们提供了几种不同的方法来创建可沿Z轴排列的重叠视图堆叠,这反过来又使我们能够为我们构建的视图定义各种覆盖和背景。让我们来探讨一下这些内置的堆叠方法,以及它们能让我们创建什么样的UI。
ZStacks
就像它的名字所暗示的那样,SwiftUI的ZStack 类型是相当于水平方向的HStack 和垂直方向的VStack 的Z轴。当在一个ZStack 中放置多个视图时,它们(默认)是从后往前渲染的,第一个视图被放置在后面。例如,在这里我们创建了一个全屏的ContentView ,它渲染了一个梯度,上面堆积了一个文本。
struct ContentView: View {
var body: some View {
ZStack {
LinearGradient(
colors: [.orange, .red],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
Text("Swift by Sundell")
.foregroundColor(.white)
.font(.title)
}
}
}
提示:你可以使用上述代码样本的PREVIEW 按钮来看看渲染后的样子。
上面的ContentView 是在所有可用的屏幕空间上渲染的,原因是LinearGradient 默认情况下总是占据尽可能多的空间,由于任何堆栈的大小默认为其子代的总大小,这导致我们的ZStack 被调整为占据同样的全屏空间。
背景修改器
然而,有时我们可能不希望一个给定的背景伸展开来,以填补所有可用的空间,虽然我们可以通过对我们的背景视图应用各种尺寸修改器来解决这个问题,但SwiftUI提供了一个内置的工具,可以自动调整一个给定视图的背景尺寸,以完全适合其父级 -background 修改器。
以下是我们如何使用该修改器来代替将我们的LinearGradient 背景直接应用到我们基于Text 的视图,这使得该背景的大小与我们的文本本身完全相同(包括其填充)。
struct ContentView: View {
var body: some View {
Text("Swift by Sundell")
.foregroundColor(.white)
.font(.title)
.padding(35)
.background(
LinearGradient(
colors: [.orange, .red],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
}
在上面的例子中,在计算我们的背景尺寸时,包含了填充物的原因是我们在添加背景之前应用了padding 修改器。要了解更多信息,请查看"SwiftUI修改器的顺序何时重要,以及为什么?"。
不过,需要指出的一点是,即使视图的background 确实会根据父视图本身调整大小,但默认情况下并没有应用任何形式的剪裁。因此,如果我们给我们的LinearGradient 一个明确的尺寸,比它的父视图大,那么它实际上会被渲染到边界之外(我们可以通过给我们基于Text 的主视图添加一个边界来清楚地证明这一点)。
struct ContentView: View {
var body: some View {
Text("Swift by Sundell")
.foregroundColor(.white)
.font(.title)
.padding(35)
.background(
LinearGradient(
colors: [.orange, .red],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.frame(width: 300, height: 300)
)
.border(Color.blue)
}
}
不过,有多种方法可以对视图进行剪裁,从而消除上述的界外渲染。例如,我们可以使用clipped 或clipShape 修改器来告诉视图对其边界应用剪切蒙版,或者我们可以给我们的视图设置圆角(这也会引入剪切)--像这样:
struct ContentView: View {
var body: some View {
Text("Swift by Sundell")
.foregroundColor(.white)
.font(.title)
.padding(35)
.background(
LinearGradient(
colors: [.orange, .red],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.frame(width: 300, height: 300)
)
.cornerRadius(20)
}
}
当然,避免在其父视图的边界之外绘制背景的最简单方法是简单地让SwiftUI布局系统自动确定每个背景的大小。这样一来,一个给定的背景的尺寸将总是与它的父视图的尺寸完全匹配。
指派覆盖物
SwiftUI也支持向视图添加覆盖层,这基本上是作为背景的逆向作用--因为它们被渲染在其父级视图之上(与我们上面探讨的尺寸行为相同)。
覆盖和背景都支持自定义对齐方式,这让我们可以决定这样的视图应该如何放置在其父级的坐标系中。对于完全可调整大小的视图(如上面的LinearGradient ),对齐方式并不重要(因为这些视图无论如何都会调整大小以适应其父视图),但对于较小的视图,指定对齐方式可以让我们将视图移动到其父视图的任何角落。
例如,我们可以在我们的ContentView ,在顶部的尾部角落添加一个星星图像覆盖。
struct ContentView: View {
var body: some View {
Text("Swift by Sundell")
.foregroundColor(.white)
.font(.title)
.padding(35)
.background(
LinearGradient(
colors: [.orange, .red],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(starOverlay, alignment: .topTrailing)
.cornerRadius(20)
}
private var starOverlay: some View {
Image(systemName: "star")
.foregroundColor(.white)
.padding([.top, .trailing], 5)
}
}
为背景指定对齐方式的方法完全相同,在使用background 修改器时传递一个alignment 参数。
一个覆盖层或背景也会继承其父辈的所有*环境值。在我们的ContentView 的例子中,这意味着我们实际上不必像上面那样应用相同的foregroundColor 修改器两次(因为前景颜色自动成为SwiftUI环境的一部分)。因此,如果我们在添加了覆盖层之后再*应用该修改器,那么同样的颜色就会同时应用于我们的文本和星形图标。
struct ContentView: View {
var body: some View {
Text("Swift by Sundell")
.font(.title)
.padding(35)
.background(
LinearGradient(
colors: [.orange, .red],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(starOverlay, alignment: .topTrailing)
.foregroundColor(.white)
.cornerRadius(20)
}
private var starOverlay: some View {
Image(systemName: "star")
.padding([.top, .trailing], 5)
}
}
条件性覆盖和背景
但有时,我们可能只想在某些条件下应用一个特定的覆盖(或背景)。例如,假设我们想在我们的ContentView 处于某种形式的加载状态时,添加第二个叠加,显示一个进度视图。
由于我们不想给我们的ContentView 本身引入任何if/else 控制流(因为这基本上会使 SwiftUI 将这两个代码路径视为两个独立的视图),我们可以通过定义一个新的@ViewBuilder- 标记的计算属性来创建这样一个有条件的叠加 - 像这样:
struct ContentView: View {
@State private var isLoading = true
var body: some View {
Text("Swift by Sundell")
.font(.title)
.padding(35)
.background(
LinearGradient(
colors: [.orange, .red],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(starOverlay, alignment: .topTrailing)
.overlay(loadingOverlay)
.foregroundColor(.white)
.cornerRadius(20)
}
...
@ViewBuilder private var loadingOverlay: some View {
if isLoading {
ProgressView()
}
}
}
请注意,在一个视图中应用多个叠加或背景是完全可以的。它们会被简单地堆叠在一起,就像使用ZStack 。
然而,我们的ProgressView ,目前不是很容易看到,而且肯定可以使用一个自己的背景。在这种情况下,我们实际上希望该背景与我们的ContentView 本身占据相同的空间,这可以使用我们在本文中最初探讨的基于ZStack 的技术来实现:
struct ContentView: View {
...
@ViewBuilder private var loadingOverlay: some View {
if isLoading {
ZStack {
Color(white: 0, opacity: 0.75)
ProgressView().tint(.white)
}
}
}
}
请注意,如果我们的应用程序支持iOS 14或更早的版本,那么我们必须将CircularProgressViewStyle(tint: .white) (使用progressViewStyle 修改器)应用于我们的进度视图,而不是使用tint ,因为该修改器是在iOS 15中引入。
iOS 15还引入了新的API,用于使用@ViewBuilder-marked closures来定义背景和覆盖。这些新API的好处是,它们允许我们在这些修改器调用中内联使用控制流(如if 语句),在我们的例子中,这将使我们能够在视图的body 中轻松地定义两个覆盖层。
struct ContentView: View {
@State private var isLoading = true
var body: some View {
Text("Swift by Sundell")
.font(.title)
.padding(35)
.background(
LinearGradient(
colors: [.orange, .red],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(alignment: .topTrailing) {
Image(systemName: "star")
.padding([.top, .trailing], 5)
}
.overlay {
if isLoading {
ZStack {
Color(white: 0, opacity: 0.75)
ProgressView().tint(.white)
}
}
}
.foregroundColor(.white)
.cornerRadius(20)
}
}
这并不是说将某些附属的视图定义委托给单独的属性不是一个好主意--事实上,这是一个很好的模式,可以帮助我们将某些庞大的body 属性分解成更容易管理的部分(而不必定义大量的新视图类型)。但有时,内联定义所有东西可能是一种方式,特别是对于更简单的覆盖,而新的基于闭合的background 和overlay 修改器肯定会使这样做更容易。
基于闭包的后向兼容性
不过,这些新的基于闭合的API仅适用于iOS 15,这有点令人遗憾,所以让我们通过解决这个问题来结束本文。重要的是要记住SwiftUI是如何被设计成可组合的,最近引入的大多数便利API只是对SwiftUI的一些旧的构建块进行组合--这也是我们自己可以做的事情。
因此,我们可以使用之前用来使某些基于async/await的系统API向后兼容的技术--也就是说,通过定义一个扩展,用我们自己的、与iOS 13兼容的版本来覆盖系统提供的API。
@available(iOS, deprecated: 15.0, message: "Use the built-in APIs instead")
extension View {
func background<T: View>(
alignment: Alignment = .center,
@ViewBuilder content: () -> T
) -> some View {
background(Group(content: content), alignment: alignment)
}
func overlay<T: View>(
alignment: Alignment = .center,
@ViewBuilder content: () -> T
) -> some View {
overlay(Group(content: content), alignment: alignment)
}
}
有了这个简单的扩展,我们现在甚至可以在需要支持早期操作系统版本的项目中使用background 和overlay 的基于闭包的变体。
总结
在使用 SwiftUI 时,背景和覆盖是两个非常有用的布局工具,与ZStack 一起,使我们能够定义各种深度堆叠的视图层次。要了解更多关于 SwiftUI 布局系统的信息,请确保查看我的三部分指南,如果你想继续探索该框架的许多其他方面,请前往SwiftUI 探索页面。
谢谢你的阅读!