为什么不是';如果调用onPreferenceChange,则为';SwiftUI中的滚动视图中有什么?

为什么不是';如果调用onPreferenceChange,则为';SwiftUI中的滚动视图中有什么?,swiftui,Swiftui,我在ScrollView中看到了一些偏好键的奇怪行为。如果我把onPreferenceChange放在ScrollView里面,它不会被调用,但是如果我把它放在外面,它会被调用 我设置了一个宽度首选项,如下所示: struct WidthPreferenceKey:PreferenceKey{ typealias值=CGFloat 静态var defaultValue=CGFloat(0) 静态函数reduce(值:inout CGFloat,nextValue:()->CGFloat){ v

我在ScrollView中看到了一些偏好键的奇怪行为。如果我把
onPreferenceChange
放在
ScrollView
里面,它不会被调用,但是如果我把它放在外面,它会被调用


我设置了一个宽度首选项,如下所示:

struct WidthPreferenceKey:PreferenceKey{
typealias值=CGFloat
静态var defaultValue=CGFloat(0)
静态函数reduce(值:inout CGFloat,nextValue:()->CGFloat){
value=nextValue()
}
}
以下简单视图不打印:

struct ContentView:View{
var body:一些观点{
滚动视图{
文本(“你好”)
.首选项(键:WidthPreferenceKey.self,值:20)
.onPreferenceChange(WidthPreferenceKey.self){
print($0)//未被调用,我们处于滚动视图中。
}
}
}
}
但这是可行的:

struct ContentView:View{
var body:一些观点{
滚动视图{
文本(“你好”)
.首选项(键:WidthPreferenceKey.self,值:20)
}
.onPreferenceChange(WidthPreferenceKey.self){
印刷品(0美元)
}
}
}

我知道我可以使用后一种方法来解决这个问题,但有时我在一个子视图中,该子视图无法访问其父滚动视图,但我仍然希望录制一个首选项键

关于如何获得
onPreferenceChange
以在
滚动视图中调用
有什么想法吗

注意:我得到了
绑定首选项宽度PreferenceKey尝试在每帧中更新多次。
当我将函数放入滚动视图时,这可能解释了发生了什么,但我无法理解


谢谢

您只能在superView中阅读,但设置后可以使用
transformPreference
进行更改

struct ContentView: View {
var body: some View {
    ScrollView {
        VStack{
        Text("Hello")
            .preference(key: WidthPreferenceKey.self, value: 20)
        }.transformPreference(WidthPreferenceKey.self, {
        $0 = 30})
    }.onPreferenceChange(WidthPreferenceKey.self) {
        print($0)
    }
}
}
最后一个值现在是
30
。希望这是你想要的

您可以从其他层读取:

  ScrollView {

        Text("Hello").preference(key: WidthPreferenceKey.self, value: CGFloat(40.0))
            .backgroundPreferenceValue(WidthPreferenceKey.self) { x -> Color in
               print(x)
                return Color.clear
        }

    }

问题似乎不一定与
ScrollView
有关,而是与您使用
PreferenceKey
有关。例如,这里是一个示例结构,其中根据
矩形的宽度设置
首选项键
,然后使用
.onPreferenceChange()
打印,所有这些都在
滚动视图
中。当您拖动
滑块更改宽度时,会更新按键并执行打印关闭

struct ContentView: View {
    @State private var width: CGFloat = 100

    var body: some View {
        VStack {
            Slider(value: $width, in: 100...200)

            ScrollView(.vertical) {
                Rectangle()
                    .background(WidthPreferenceKeyReader())
                    .onPreferenceChange(WidthPreferenceKey.self) {
                        print($0)
                }
            }
            .frame(width: self.width)
        }
    }
}

struct WidthPreferenceKeyReader: View {
    var body: some View {
        GeometryReader { geometry in
            Rectangle()
                .fill(Color.clear)
                .preference(key: WidthPreferenceKey.self, value: geometry.size.width)
        }
    }
}
正如您所注意到的,当密钥第一次尝试设置时,控制台会打印“绑定首选项宽度首选项密钥尝试每帧更新多次”,但随后会立即设置一个实际值,并继续动态更新


您实际试图设置的值是什么?在
.onPreferenceChange()
中您试图做什么?

我认为您的示例中没有调用onPreferenceChange,因为它的函数与preference(键…)有很大的不同

首选项(键:…)为在其上使用的视图设置首选项值。 而onPreferenceChange是在父视图上调用的函数,父视图是视图树层次结构中较高位置的视图。它的功能是遍历所有子项和子项,并收集它们的首选项(键:)值。当它找到一个值时,它将使用PreferenceKey中的reduce函数来处理这个新值和所有已收集的值。一旦收集并减少了所有值,它将对结果执行onPreference闭包

在第一个示例中,从未调用此闭包,因为文本(“Hello”)视图没有设置首选项键值的子对象(实际上,该视图根本没有子对象)。在第二个示例中,滚动视图有一个子视图,用于设置其首选项值(文本视图)

所有这些都不能解释每帧多次的错误——这很可能是不相关的

最新更新(2020年4月24日): 在类似的情况下,我可以通过改变PreferenceData的可等式条件来诱导onPreferenceChange的调用。首选项数据需要是相等的(可能是为了检测它们的变化)。但是,锚类型本身不再是相等的。要提取锚类型中包含的值,需要使用GeometryProxy。通过GeometryReader获得GeometryProxy。为了不干扰视图的设计,我在PreferenceData结构的Equalable函数中生成了一个GeometryReader,将其中一些视图封装到GeometryReader中:

struct ParagraphSizeData: Equatable {
  let paragraphRect: Anchor<CGRect>?

  static func == (value1: ParagraphSizeData, value2: ParagraphSizeData) -> Bool {

    var theResult : Bool = false
    let _ = GeometryReader { geometry in
       generateView(geometry:geometry, equality:&theResult)
    }

    func generateView(geometry: GeometryProxy, equality: inout Bool) -> Rectangle {
       let paragraphSize1, paragraphSize2: NSSize

      if let anAnchor = value1.paragraphRect { paragraphSize1 = geometry[anAnchor].size }
       else {paragraphSize1 = NSZeroSize }
       if let anAnchor = value2.paragraphRect { paragraphSize2 = geometry[anAnchor].size }
       else {paragraphSize2 = NSZeroSize }

       equality = (paragraphSize1 == paragraphSize2)
       return Rectangle()
    }

   return theResult     
  }

}
struct ParagraphSizeData:equalable{
让段落直截了当:锚?
静态函数==(值1:ParagraphSizeData,值2:ParagraphSizeData)->Bool{
var结果:Bool=false
设u=GeometryReader{geometry in
generateView(几何图形:几何图形、等式和结果)
}
func generateView(几何图形:GeometryProxy,等式:inout Bool)->矩形{
让第1段,第2段:NSSize
如果让anAnchor=value1.paragraphRect{paragraphSize1=geometry[anAnchor].size}
else{paragraphSize1=NSZeroSize}
如果让anAnchor=value2.paragraphRect{paragraphSize2=geometry[anAnchor].size}
else{paragraphSize2=NSZeroSize}
相等=(第1段==第2段)
返回矩形()
}
返回结果
}
}

带着亲切的问候

我很长时间以来一直在努力解决这个问题,并找到了如何处理这个问题的方法,尽管我所使用的方法只是一种变通方法

使用带有标志的
onAppear
ScrollView
,使其子项显示出来

或者

使用
列表
代替它。
它具有滚动功能,您可以使用它自己的功能和UI方面的UITableView外观对其进行自定义。最重要的是它按照我们的预期工作

[如果您有时间阅读更多]<
...
@State var isShowingContent = false
...

ScrollView {
    if isShowingContent {
        ContentView()
    }
}
.onAppear {
     self.isShowingContent = true
}
struct ContentView: View {
    var body: some View {
        ScrollView {
            Text("Hello")
                .preference(key: WidthPreferenceKey.self, value: 20)
                .onPreferenceChange(WidthPreferenceKey.self) {
                    print($0) // Not being called, we're in a scroll view.
                }
        }
    }
}
struct ContentView: View {
    var body: some View {
        ScrollView {
            Text("Hello")
                .preference(key: WidthPreferenceKey.self, value: 20)
        }
        .onPreferenceChange(WidthPreferenceKey.self) {
            print($0)
        }
    }
}