SwiftUI视图与修饰符 SwiftUI视图与修饰符

SwiftUI视图与修饰符

hudson 译 原文

至少从架构的角度来看,SwiftUI最有趣的方面之一是它本质上如何将视图视为数据。毕竟,SwiftUI视图不是屏幕上渲染的像素的直接表示,而是对给定UI应该如何工作、外观和行为的描述。

在如何构建视图代码时,这种非常数据驱动的方法给了我们很大的灵活性——以至于人们甚至可能会开始质疑将一个UI定义为视图类型与将相同的代码作为修饰符实现之间到底有什么区别。

以下面FeaturedLabel视图为例——它在将星形图片添加到给定文本前面,并应用特定的前景色和字体,使该文本脱颖而出,成为“特色”:

1
2
3
4
5
6
7
8
9
10
11
12
swift复制代码struct FeaturedLabel: View {
var text: String

var body: some View {
HStack {
Image(systemName: ”star“)
Text(text)
}
.foregroundColor(.orange)
.font(.headline)
}
}

虽然上述内容可能看起来像典型的自定义视图,但使用“修改器风格”的View 扩展,也可以轻松实现完全相同的界面效果——像这样:

1
2
3
4
5
6
7
8
9
10
swift复制代码extension View {
func featured() -> some View {
HStack {
Image(systemName: ”star“)
self
}
.foregroundColor(.orange)
.font(.headline)
}
}

当放在示例ContentView中时,这两个不同的解决方案并排看起来如下:

1
2
3
4
5
6
7
8
9
10
11
swift复制代码struct ContentView: View {
var body: some View {
VStack {
// View-based version:
FeaturedLabel(text: ”Hello, world!“)

// Modifier-based version:
Text(”Hello, world!“).featured()
}
}
}

两种解决方案之间的一个关键区别是,后者可以应用于任何视图,而前者只允许我们创建基于String的特色标签。不过,我们可以通过将我们的FeaturedLabel转换为自定义容器视图来解决这个问题,该视图接受任何符合视图的内容,而不仅仅是一个普通的字符串:

1
2
3
4
5
6
7
8
9
10
11
12
swift复制代码struct FeaturedLabel<Content: View>: View {
@ViewBuilder var content: () -> Content

var body: some View {
HStack {
Image(systemName: ”star“)
content()
}
.foregroundColor(.orange)
.font(.headline)
}
}

上面,我们将ViewBuilder属性添加到 content闭包中,便可以利用SwiftUI的视图构建API的全部功能(例如, 可以为每个FeaturedLabel构建内容时使用ifswitch语句)。

不过,我们可能仍然希望使用一个字符串轻松初始化 FeaturedLabel 实例,而不是总是传递包含Text视图的闭包。谢天谢地,使用类型约束,可以轻松实现:

1
2
3
4
5
6
7
swift复制代码extension FeaturedLabel where Content == Text {
init(_ text: String) {
self.init {
Text(text)
}
}
}

上图中,我们使用下划线来删除文本的外部参数标签,以模仿SwiftUI自己的内置便利API,如ButtonNavigationLink等类型工作的方式。

有了这些改变,两种解决方案现在都具备完全相同的灵活性,并且既可以轻松地用于创建基于文本的标签,也可渲染任何类型的SwiftUI视图内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
swift复制代码struct ContentView: View {
@State private var isToggleOn = false

var body: some View {
VStack {
// Using texts:
Group {
// View-based version:
FeaturedLabel(”Hello, world!“)

// Modifier-based version:
Text(”Hello, world!“).featured()
}

// Using toggles:
Group {
// View-based version:
FeaturedLabel {
Toggle(”Toggle“, isOn: $isToggleOn)
}

// Modifier-based version:
Toggle(”Toggle“, isOn: $isToggleOn).featured()
}
}
}
}

此时我们可能会真正开始问自己——将UI定义为视图与修饰符到底有什么区别? 除了代码风格和结构外,真的有什么实际的差异吗?

嗯,那状态呢? 假设我们想让新的特色标签在首次出现时自动淡入? 这需要我们用@State-来标记opacity属性,然后在onAppear闭包中实现动画效果——例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
swift复制代码struct FeaturedLabel<Content: View>: View {
@ViewBuilder var content: () -> Content
@State private var opacity = 0.0

var body: some View {
HStack {
Image(systemName: ”star“)
content()
}
.foregroundColor(.orange)
.font(.headline)
.opacity(opacity)
.onAppear {
withAnimation {
opacity = 1
}
}
}
}

起初,参与SwiftUI状态管理系统似乎只有适当的视图类型才能做到,但事实证明,修饰符具有完全相同的功能——只要我们将此类修饰符定义为适当的符合 ViewModifier协议的类型,而不仅仅是使用View协议扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
swift复制代码struct FeaturedModifier: ViewModifier {
@State private var opacity = 0.0

func body(content: Content) -> some View {
HStack {
Image(systemName: ”star“)
content
}
.foregroundColor(.orange)
.font(.headline)
.opacity(opacity)
.onAppear {
withAnimation {
opacity = 1
}
}
}
}

有了上述功能,我们现在可以用调用将新的 FeaturedModifier 添加到当前视图中来,取代之前的featured方法实现——两种方法再次产生完全相同的最终结果:

1
2
3
4
5
swift复制代码extension View {
func featured() -> some View {
modifier(FeaturedModifier())
}
}

同样值得注意的是,在ViewModifier类型中包装代码时,代码是延迟加载的,即在需要时计算代码,而不是在首次添加修饰符时就执行的,某些情况下,性能可能会有所不同。

因此,无论我们是想更改视图的样式或结构,还是引入新的状态,SwiftUI视图和修饰符都具有完全相同的功能。这点开始变得明显。但这让我们想到了下一个问题——如果这两种方法之间没有实际差异,我们如何在它们之间做出选择?

至少对我来说,这一切都归结为结果视图层次结构。虽然从技术上讲,在HStack中包装一个特色标签改变了视图层次结构,以添加星形图像,但从概念上讲,这更多的是关于样式,而不是结构。当将featured修饰符应用于视图时,从任何有意义方式说上,其布局或在视图层次结构中的位置并没有真正改变 ——至少从高级的角度来看,它仍然只是具有完全相同整体布局的单一视图。

不过,情况并非总是如此。看看另一个例子,它应该更清楚地说明视图和修饰符之间的潜在结构差异。

在这里,我们编写了一个SplitView容器,它采用两个视图——一个是前导视图,一个是尾随视图——然后将它们并排渲染,中间是一个分隔符控件,同时最大化其frame,最终它们将平均分隔可用空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
swift复制代码struct SplitView<Leading: View, Trailing: View>: View {
@ViewBuilder var leading: () -> Leading
@ViewBuilder var trailing: () -> Trailing

var body: some View {
HStack {
prepareSubview(leading())
Divider()
prepareSubview(trailing())
}
}

private func prepareSubview(_ view: some View) -> some View {
view.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

就像以前一样,绝对可以使用基于修饰符的方法实现完全相同的结果——这可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
swift复制代码extension View {
func split(with trailingView: some View) -> some View {
HStack {
maximize()
Divider()
trailingView.maximize()
}
}

func maximize() -> some View {
frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

然而,如果再次将两个解决方案放在同一个示例ContentView中,那么可以看到,这次这两种方法在结构和清晰度方面看起来确实大不相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
swift复制代码struct ContentView: View {
var body: some View {
VStack {
// View-based version:
SplitView(leading: {
Text(”Leading“)
}, trailing: {
Text(”Trailing“)
})

// Modifier-based version:
Text(”Leading“).split(with: Text(”Trailing“))
}
}
}

看看上面的基于视图的调用方法,很明显,两个文本被包装在一个容器中,也很容易判断哪个是前导视图,哪个是尾随视图。

不过这次,不能对基于修饰符的版本说同样的事情,我们确实需要知道,应用修饰符的视图 是前导视图。此外,也无法判断这两个文本最终会被包裹在什么样的容器中。看起来更像是使用尾随标签以某种方式为前导标签增加某种风格 ,但事实并非如此。

虽然可以尝试用更详细的API命名来解决清晰度问题,但核心问题仍然存在——在这种情况下,基于修饰符的版本没有正确显示结果的视图层次结构。因此,在上述情况下,当我们在父容器中包装多个兄弟姐妹时,选择基于视图的解决方案通常会给我们一个更清晰的最终结果。

另一方面,如果我们所做的只是将一组样式应用于单个视图,那么将其实现为“修改器类似”的扩展,或使用正确的ViewModifier类型,将是最常要走的路。对于介于两者之间的一切——例如我们早期的“特色标签”示例——这一切都归结为代码风格和个人偏好,即哪种解决方案最适合每个给定项目。

看看SwiftUI的内置API是如何设计的——容器(如HStackVStack)是视图,而样式API(如填充和前景颜色)是作为修饰符实现的。因此,如果在项目中尽可能多地遵循相同的方法,那么最终UI代码可能会感觉一致,且与SwiftUI本身一致。

我希望你发现这篇文章有趣且有用。如果有任何问题、评论或反馈,请随时在Mastodon 上找到我,或通过电子邮件与我联系。

感谢您的阅读!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%