列表内的 SWIftUI anchorPreference

Posted

技术标签:

【中文标题】列表内的 SWIftUI anchorPreference【英文标题】:SWiftUI anchorPreference inside List 【发布时间】:2020-12-21 02:34:51 【问题描述】:

我使用anchorPreferences 来设置GeometryReaderheight 以适应其内容的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()
    

【问题讨论】:

【参考方案1】:

实际上我们不知道 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)
    


【讨论】:

but preferences really required to be written only once - 因此“尝试每帧更新多次”。是坏事吗? 我不认为这是,这只是一个警告,意味着您存储的一些偏好值可能会丢失,而在需要时不应用。跨度> 嗯,有道理。谢谢! 谢谢@Asperi!有没有办法在 InnerView 中“生成”“idx”,这样我就不必将其作为参数发送?【参考方案2】:

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
    

【讨论】:

以上是关于列表内的 SWIftUI anchorPreference的主要内容,如果未能解决你的问题,请参考以下文章

对 SwiftUI 中水平滚动视图内的列表不起作用的操作

如何使用滚动视图单击列表部分内的项目以导航到详细信息页面 SwiftUI

访问全局函数内的环境变量 - SwiftUI + CoreData

SwiftUI 从列表中编辑结构

如何防止 TextField 在 SwiftUI 列表中消失?

如何将字典项分别添加到 SwiftUI 列表中