SwiftUI

SwiftUI中Preferences的使用

2021-09-13  本文已影响0人  MambaYong

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 放置在座标系合适的位置上。

总结:

上一篇下一篇

猜你喜欢

热点阅读