列表中的 SWiftUI anchorPreference
SWiftUI anchorPreference inside List
我正在使用 anchorPreferences
设置 GeometryReader
的 height
以适应其内容的 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()) { }
}
}
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
}
}
我正在使用 anchorPreferences
设置 GeometryReader
的 height
以适应其内容的 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()) { }
}
}
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
}
}