简体   繁体   中英

SWiftUI anchorPreference inside List

I'm using anchorPreferences to set the height of GeometryReader to fit the height of its content. The issue I'm experiencing is, after scrolling up and down the List a few times, the app freezes, the design gets messy, and I get the following message in the console:

Bound preference CGFloatPreferenceKey tried to update multiple times per frame.

Any ideas how I can fix this?

IMPORTANT NOTE: I've simplified my design as much as I could.

在此处输入图像描述

Here is the code:

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()
    }
}

Actually we don't know how many times on stack SwiftUI can render a body of our custom view, but preferences really required to be written only once (and then they should be transformed, which is more complex).

The possible solution is to use different type of container in Preferences, so values not re-written but accumulated.

Here is modified parts of your code. Tested with 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)
    }
}

backup

The code in your List is trying to do too much. One of the benefits of SwiftUI is that you don't need to manually set the height of your views. Maybe the GeometryReader is just left over from when you simplified your example, but in this simplified case, you don't need it (or Preferences). This is all you need:

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()
    }
}

If for whatever reason you do need to listen for changes via onPreferenceChange , or for that matter, onChange(of:) , you need to ensure that any GUI-impacting changes happen on the main thread. The following change would silence the warnings you're seeing in your original code:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM