问题描述
我正在使用 anchorPreferences
设置 height
的 GeometryReader
以适合其内容的 height
。我遇到的问题是,在 List
上下滚动几次后,应用程序冻结,设计变得混乱,并且我在控制台中收到以下消息:
Bound preference CGFloatPreferenceKey tried to update multiple times per frame.
有什么想法可以解决这个问题吗?
重要提示:我已经尽可能地简化了我的设计。
代码如下:
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
}
}