在
SwiftUI
中,父View
可以分享environment
给子View
使用,同时订阅environment
的变化,但是有时候子View
需要传递数据给父View
,在SwiftUI
这种情况通常使用Preferences
。
import SwiftUI
struct ContentView: View {
let messages: [String] = ["one","two","three"]
var body: some View {
NavigationView {
List(messages, id: \.self) { message in
Text(message)
}.navigationBarTitle("Messages")
}.onPreferenceChange(NavigationBarTitleKey.self) { title in
// title即为子View提供的值
print(title) // 打印 three
}
}
}
// 定义了一个PreferenceKey
struct NavigationBarTitleKey: PreferenceKey {
// 默认值
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
extension View {
func navigationBarTitle(_ title: String) -> some View {
self.preference(key: NavigationBarTitleKey.self, value: title)
}
}
使用preferences
时,需要声明一个遵守PreferenceKey
协议的Struct
,PreferenceKey
协议有二个必要的实现,一个是defaultValue
默认值,另外一个是reduce
方法。
reduce方法
reduce
方法在Swift
中非常常见,这里的用处是当有多个子View
都给父View
传递数据时,父View
最后是只能接受一个数据,而reduce
就是将子View
提供的多个数据进行“操作”,降维为一个数据提供给父View
使用,PreferenceKey
的reduce
方法包含两个参数:当前的value
,和下一个要合并的值nextValue
,这二个参数是子View
从上到下提供的。
上面代码中
List
根据messages
数组的个数循环显示Text
文本,每个Text
文本都调用了preference(key: value:)
方法来向父View
提供title
数据,当父View
调用onPreferenceChange
方法时,会触发对应的PreferenceKey
中的reduce
方法(不调用是不会触发的),这里是简单的返回了nextValue
,也就是List
中最后一个Text
发出的title
值(打印three)。
获取子View的尺寸
在
SwiftUI
中,子View
要想获得父View
的尺寸使用GeometryReader
,当父View
想知道子View
的尺寸时就可采用Preferences
。
struct MainButtonView: View {
// 通过PreferenceKey能让父view拿到子view包装的信息
private struct SizeKey: PreferenceKey {
static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
value = value ?? nextValue()
}
}
@State private var height: CGFloat?
var title: String
var type: MainButtonType
// 按钮点击的回调
var callback: () -> Void
var body: some View {
Button(action: {
callback()
}) {
HStack { // 外层HStack
ZStack(alignment: .center) { // 内层ZStack
HStack { // 内层HStack1
Spacer()
Text(title)
.font(.uiButtonLabelLarge)
.foregroundColor(.buttonText)
.padding(15)
.background(GeometryReader { proxy in
// 将HStack的尺寸传递给了父ZStack,然后Iamge使用了这个尺寸来设置宽高
Color.clear.preference(key: SizeKey.self, value: proxy.size)
})
Spacer()
}
if type.hasArrow {
HStack { // 内层HStack2
Spacer()
Image(systemName: "arrow.right")
.font(Font.system(size: 14, weight: .bold))
.frame(width: height, height: height)
.foregroundColor(type.color)
.background(
Color.white
.cornerRadius(9)
.padding(12)
)
}
}
}
.frame(height: height)
.background(
RoundedRectangle(cornerRadius: 9)
.fill(type.color)
)
.onPreferenceChange(SizeKey.self) { size in
height = size?.height
}
}
}
}
}
MainButtonView(title: "Got It!", type: .primary(withArrow: true), callback: {})
.padding(20)
.background(Color.backgroundColor)
这里述说一下完整的布局流程:
1.MainButtonView
在将屏幕宽度扣除掉左右2个方向的padding=20
后,将这个剩下的宽度尺寸和整个屏幕高度尺寸作为提议,向外层的HStack
请求尺寸。
2.紧接着外层的HStack
会继续像内层的ZStack
请求尺寸,ZStack
会继续像内层的二个HStack
请求尺寸,此时ZStack
提议给内层的尺寸依旧是上述1中的提议尺寸。
3.由于是ZStack
,内存的HStack1
和HStack2
会拿着提议尺寸继续找自己的子View
请求尺寸。
4.HStack1
内的Text
会首先尊重提议的宽度尺寸,并根据是否换行或者省略的方式来显示自己,由于此时HStack1
提议的宽度尺寸较大,此时Text
会根据显示的文字将实际的宽度和高度反馈给HStack1
,这样HStack1
就确定了自己的尺寸。
5.HStack1
确定了自己的尺寸后,Text
通过GeometryReader
拿到了HStack1
确定好的尺寸,并通过SizeKey
告诉期上面的给父View
。
6.由于ZStack
调用了onPreferenceChange
方法,这样ZStack
就获得了HStack1
的尺寸,并赋值给了height
变量,SwiftUI
此时会刷新整个View
,下面的HStack2
内的布局和上面HStack1
差不多,只不多此时Image
的宽高已有了指定的尺寸(Text
的高度)。
7.确定好尺寸的HStack1
和HStack2
将自己的尺寸上报给ZStack
,ZStack
确定好尺寸在上报给外层HStack
,这样整个MainButtonView
就完成了尺寸布局。

SwiftUI
遵循的布局规则,可以总结为 “协商解决,层层上报”:父层级的View
根据某种规则,向子View
“提议” 一个可行的尺寸;子View
以这个尺寸为参考,按照自己的需求进行布局:或占满所有可能的尺寸 (比如Rectangle
和Circle
),或按照自己要显示的内容确定新的尺寸 (比如Text
),或把这个任务再委托给自己的子View
继续进行布局 (比如各类Stack View
)。在子View
确定自己的尺寸后,它将这个需要的尺寸汇报回父View
,父View
最后把这个确定好尺寸的子View
放置在座标系合适的位置上。
总结:
- 本文简述了
Preferences
的使用,并说明了PreferenceKey
协议中reduce
方法的实现原理。 - 利用
Preferences
在实际开发中获取子View
的尺寸。 - 简述了
SwiftUI
的布局规则。
网友评论