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