列表中的 SWiftUI anchorPreference

问题描述

我正在使用 anchorPreferences 设置 heightGeometryReader 以适合其内容height。我遇到的问题是,在 List 上下滚动几次后,应用程序冻结,设计变得混乱,并且我在控制台中收到以下消息:

Bound preference CGFloatPreferenceKey tried to update multiple times per frame.

有什么想法可以解决这个问题吗?

重要提示:我已经尽可能地简化了我的设计。

enter image description here

代码如下:

struct ListAndPreferences: View {
    var body: some View {
        List(1..<35) { idx in
            HStack {
                Text("idx: \(idx)")
                
                InnerView(idx: idx)
            }
        }
    }
}

struct InnerView: View {
    @State var height: CGFloat = 0
    var idx: Int
    
    var body: some View {
        GeometryReader { proxy in
            generateContent(maxWidth: proxy.frame(in: .global).size.width)
                .anchorPreference(key: CGFloatPreferenceKey.self,value: Anchor<CGRect>.source.bounds,transform: { anchor in
                    proxy[anchor].size.height
                })
        }
        .frame(height: height)
        .onPreferenceChange(CGFloatPreferenceKey.self,perform: { value in
            height = value
        })
    }
    
    private func generateContent(maxWidth: CGFloat) -> some View {
            vstack {
                HStack {
                    Text("hello")
                        .padding()
                        .background(Color.purple)
                    
                    Text("world")
                        .padding()
                        .background(Color.purple)
                }               
            }
            .frame(width: maxWidth)
    }
}

struct CGFloatPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat,nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

解决方法

实际上我们不知道 SwiftUI 可以在堆栈上渲染多少次自定义视图的主体,但是首选项确实只需要编写一次(然后它们应该被转换,这更复杂)。

可能的解决方案是在 Preferences 中使用不同类型的容器,这样值不会被重写而是累积。

这是您的代码的修改部分。使用 Xcode 12.1 / iOS 14.1 测试

// use dictionary to store calculated height per view idx
struct CGFloatPreferenceKey: PreferenceKey {
    static var defaultValue: [Int: CGFloat] = [:]
    static func reduce(value: inout [Int: CGFloat],nextValue: () -> [Int: CGFloat]) {
        value.merge(nextValue()) { $1 }
    }
}

struct InnerView: View {
    @State var height: CGFloat = 0
    var idx: Int
    
    var body: some View {
        GeometryReader { proxy in
            generateContent(maxWidth: proxy.frame(in: .global).size.width)
                .anchorPreference(key: CGFloatPreferenceKey.self,value: Anchor<CGRect>.Source.bounds,transform: { anchor in
                    [idx: proxy[anchor].size.height]
                })
        }
        .frame(minHeight: height)
        .onPreferenceChange(CGFloatPreferenceKey.self,perform: { value in
            height = value[idx] ?? .zero
        })
    }
    
    private func generateContent(maxWidth: CGFloat) -> some View {
            VStack {
                HStack {
                    Text("hello")
                        .padding()
                        .background(Color.purple)
                    
                    Text("world")
                        .padding()
                        .background(Color.purple)
                }
            }
            .frame(width: maxWidth)
    }
}

,

您的 List 中的代码尝试做的太多了。 SwiftUI 的好处之一是您无需手动设置视图的高度。也许 GeometryReader 只是在您简化示例时遗留下来的,但在这种简化的情况下,您不需要它(或首选项)。这就是您所需要的:

import SwiftUI

struct ListAndPreferences: View {
    var body: some View {
        List(1..<35) { idx in
            HStack {
                Text("idx: \(idx)")
                InnerView()
            }
        }
    }
}

struct InnerView: View {
    var body: some View {
        VStack {
            HStack {
                Text("hello")
                    .padding()
                    .background(Color.purple)

                Text("world")
                    .padding()
                    .background(Color.purple)
            }
        }.frame(maxWidth: .infinity)
    }
}

struct InnerView_Previews: PreviewProvider {
    static var previews: some View {
        ListAndPreferences()
    }
}

如果您出于某种原因需要通过 onPreferenceChange 监听更改,或者就此而言,onChange(of:),您需要确保任何影响 GUI 的更改发生在主线程上。以下更改将使您在原始代码中看到的警告静音:

.onPreferenceChange(CGFloatPreferenceKey.self) { value in
    DispatchQueue.main.async {
        height = value
    }
}