SwiftUI中Preference的理解与使用(ScrollView偏移量示例)

在 SwiftUI 中,Preference用于从视图层次结构的较深层次向上传递信息到较浅层次。这通常用于在父视图中获取子视图的属性或状态,而不需要使用状态管理工具如@State@ObservableObjectPreference特别用于自定义布局或组件,其中子视图需要向父视图报告其尺寸或其他属性。

如何使用 Preference

使用Preference通常涉及以下几个步骤:

1. 定义PreferenceKey

创建一个遵循PreferenceKey协议的结构体。这个结构体定义了如何合并来自多个视图的值。

struct WidthPreferenceKey: PreferenceKey {
  static var defaultValue: CGFloat? = nil  // 默认值

  static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
    value = value ?? nextValue()
  }
}

defaultValue
defaultValuePreferenceKey协议中必须提供的属性。它定义了存储在preferences中的数据类型的初始值。这个值是在数据类型的上下文中使用的默认值,用于在没有实际值可用时提供一个基线值。

reduce(value:nextValue:)
reduce方法是PreferenceKey协议中必须实现的方法。这个方法定义了如何将新的值合并到现有的值中。当从子视图向上传递多个值时,reduce方法会被调用来决定最终存储的值。

  • value: 这是一个inout参数,表示当前累积的preference值。
  • nextValue: 这是一个闭包,当调用时,它返回当前视图提供的新值。

reduce方法的实现取决于你的具体需求。例如,如果你想要从多个子视图中获取最大的值,你可以实现reduce方法来比较当前值和新值,并存储较大的那个。

比如下面这个例子中,defaultValue被设置为0,这是一个合理的起始值,因为我们正在寻找最大宽度。reduce方法通过比较已经累积的最大宽度和新的宽度值,并保留较大的那个,来更新这个值。

struct MaxWidthPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0  // 默认值为0

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = max(value, nextValue())  // 取最大值
    }
}

2. 设置 Preference:

在子视图中,使用.preference(key:value:)修饰符来设置其preference值。

  var body: some View {
    Text("Hello, World!")
      .background(GeometryReader { geometry in
        Color.clear
          .preference(key: WidthPreferenceKey.self, value: geometry.size.width)
      })
  }

3. 读取 Preference:
在父视图中,使用.onPreferenceChange(_:perform:)修饰符来读取preference值。

  var body: some View {
    VStack {
      Text("Hello, World!")
        .background(GeometryReader { geometry in
          Color.clear
            .preference(key: WidthPreferenceKey.self, value: geometry.size.width)
      })
    }
    .onPreferenceChange(WidthPreferenceKey.self) { width in
        if let width = width {
            print("Received width: \(width)")
        }
    }
  }

在这个例子中,Text视图的宽度被动态设置为与其内容的宽度相匹配。

计算ScrollView的偏移量offsetY

有的时候我们需要在上下滚动ScrollView的时候实时知道offset,从而做一些其他逻辑等。下面的代码就采用了Preference实现这个功能,一起来看看吧。先把代码上全。

struct PreferenceDemo: View {
  @Namespace var scrollViewSpace

  var body: some View {
    ScrollView(.vertical, showsIndicators: false) {
      VStack {
        ForEach(0..<100) { index in
          Text("This is item \(index)")
            .font(.headline)
            .frame(height: 100)
            .frame(maxWidth: .infinity)
            .background(Color.white)
            .cornerRadius(10)
            .shadow(radius: 10)
            .padding()
            .id(index)
        }

      }
      .background(GeometryReader {
          Color.clear.preference(key: ScrollOffsetPreferenceKey.self, value: -$0.frame(in: .named(scrollViewSpace)).minY)
      })
      .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
        print("offSetY: \(value)")
      }
    }
    .coordinateSpace(name: scrollViewSpace)
  }
}

private struct ScrollOffsetPreferenceKey: PreferenceKey {
  static var defaultValue = CGFloat.zero
  static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
    value += nextValue()
  }
}

上面的实现就是按照文中说的三部曲,自定义的ScrollOffsetPreferenceKey中,将每次进来的值进行叠加。

在设置.preference(key:value:)修饰符的地方,我们传入的当前的geometryminY值,这个minY值是自定义坐标空间的值,注意我们设置了.coordinateSpace(name: scrollViewSpace)ScrollView,关于坐标系这块可以参考这篇文章

这里得到minY后,我们在前面加了一个负号,因为在坐标系中向上滑动得到的是负值,我们人为再转一下,对于偏移量,一般向上为正,向下为负,或者向左为正,向右为负,不过这也不是决定的。

.onPreferenceChange修饰符中拿个偏移量后,就可以进行下一步的操作了。

效果如下:
在这里插入图片描述

写在最后

PreferenceSwiftUI中是一个强大的工具,允许开发者在视图层次结构中向上传递数据,而不需要复杂的状态管理。这对于创建高度可定制的UI组件非常有用,尤其是在布局和尺寸调整方面。

最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值