SwiftUI PreferenceKey

下图所示是一个常规的登录界面,只需少量代码即可实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ContentView : View {

@State private var email = ""
@State private var password = ""

var body: some View {
Form {
HStack {
Text("电子邮箱")
TextField("请输入", text: $email)
.textFieldStyle(RoundedBorderTextFieldStyle())
}

HStack {
Text("密码")
TextField("请输入", text: $email)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
}

但是通常我们为了美观,会将左边的文字列等宽对齐,也许固定宽度是个不错的想法,但是可扩展性太差,我们如何解决这个问题呢?

常规的思路就是,获取文字列所有的内容的宽度,取最大值,重绘界面即可。那么问题来了,如何获取这个最大值呢?答案就是 PreferenceKey,它可以收集视图树中子视图的数据,回传给父视图(跨层级亦可)。这里我们需要获取尺寸,还用到了 GeometryReader

改造后的效果和代码如下:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
struct ContentView : View {

@State private var email = ""
@State private var password = ""
// 保存、更新文字列所需要的合适宽度,这里是最大值
@State private var textWidth: CGFloat?

var body: some View {
Form {
HStack {
Text("电子邮箱")
.frame(width: textWidth, alignment: .leading)
.background(TextBackgroundView())
TextField("请输入", text: $email)
.textFieldStyle(RoundedBorderTextFieldStyle())
}

HStack {
Text("密码")
.frame(width: textWidth, alignment: .leading)
.background(TextBackgroundView())
TextField("请输入", text: $email)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
.onPreferenceChange(TextWidthPreferenceKey.self) { (value) in
print(value)
textWidth = value.max()
}
}
}

struct TextBackgroundView: View {
var body: some View {
GeometryReader { gr in
Rectangle()
.fill(Color.clear)
.preference(key: TextWidthPreferenceKey.self,
value: [gr.size.width])
}
}
}

struct TextWidthPreferenceKey: PreferenceKey {
// 偏好值没有被设定时,使用默认值
static var defaultValue: [CGFloat] = []

// 收集视图树中的数据
// nextValue 的闭包是惰性调用的,只有需要用到它时才会去获取相应的值
static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
value.append(contentsOf: nextValue())
}
}

有一点需要注意,为什么我们要使用 TextBackgroundView 来作为背景回传所需要的值呢?因为我们期望 Form 列表的布局是根据子视图的布局来更新的,而子视图又依赖父视图传入的宽度值,这样形成了一个得不到结果的死循环。而 TextBackgroundView 可以打破这个僵局,父视图所依赖的布局不再是文字的布局,而是背景层的视图布局。

补充说明一下,SwiftUI 的视图层级是不同于 UIKit 的,在 UIKit 中,背景是控件的属性,而 SwiftUI 中,.background 会在视图树中生成一个新的视图,是独立与所修饰的控件的。

另外有一点令我不解的是,既然我是要获取最大宽度,只需要在 TextWidthPreferenceKey 将关联类型设置为 CGFloat 即可,在 reduce 方法中写入 value = max(value, nextValue()),然后在 onPreferenceChange 中将最大值传给 textWidth ,这样不是更简单吗?但是事与愿违,这样达不到我们想要的效果,观察控制台,我发现确实可以获取到最大宽度值,但是不会更新视图布局,百思不得其解,网上也没找到合理的解释。暂且放下,以后再研究一下这个问题。

参考:

您的支持将鼓励我继续创作!
0%