美文网首页SwiftUI
SwiftUI中Preferences的使用

SwiftUI中Preferences的使用

作者: MambaYong | 来源:发表于2021-09-13 11:02 被阅读0次

    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协议的StructPreferenceKey协议有二个必要的实现,一个是defaultValue默认值,另外一个是reduce方法。

    reduce方法

    reduce方法在Swift中非常常见,这里的用处是当有多个子View都给父View传递数据时,父View最后是只能接受一个数据,而reduce就是将子View提供的多个数据进行“操作”,降维为一个数据提供给父View使用,PreferenceKeyreduce方法包含两个参数:当前的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,内存的HStack1HStack2会拿着提议尺寸继续找自己的子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.确定好尺寸的HStack1HStack2将自己的尺寸上报给ZStackZStack确定好尺寸在上报给外层HStack,这样整个MainButtonView就完成了尺寸布局。

    image.png

    SwiftUI遵循的布局规则,可以总结为 “协商解决,层层上报”:父层级的View 根据某种规则,向子 View“提议” 一个可行的尺寸;子View以这个尺寸为参考,按照自己的需求进行布局:或占满所有可能的尺寸 (比如RectangleCircle),或按照自己要显示的内容确定新的尺寸 (比如Text),或把这个任务再委托给自己的子View 继续进行布局 (比如各类Stack View )。在子 View确定自己的尺寸后,它将这个需要的尺寸汇报回父 View,父 View最后把这个确定好尺寸的子View 放置在座标系合适的位置上。

    总结:

    • 本文简述了Preferences的使用,并说明了PreferenceKey协议中reduce方法的实现原理。
    • 利用Preferences在实际开发中获取子View的尺寸。
    • 简述了SwiftUI的布局规则。

    相关文章

      网友评论

        本文标题:SwiftUI中Preferences的使用

        本文链接:https://www.haomeiwen.com/subject/bfegwltx.html