Home > front end >  SwiftUI read rect of child containing dynamic content
SwiftUI read rect of child containing dynamic content

Time:08-10

So I've seen some approach to read the CGRect of a swiftui view using a technique where you setup a background containing a GeometryReader that is used to read the size of the view the background is set up. My issue is that this doesn't work if the view you're setting the background to has some ifs in it for example. See the code below for reference:


import SwiftUI

struct RectPreferenceKey: PreferenceKey {
    static var defaultValue = CGRect.zero
    
    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}

struct ChildRectReaderModifier: ViewModifier {
    @Binding var rect: CGRect
    
    func body(content: Content) -> some View {
        content
            .background(
                GeometryReader { proxy in
                    Color.clear.preference(
                        key: RectPreferenceKey.self,
                        value: proxy.frame(in: .global)
                    )
                }
            )
            .onPreferenceChange(RectPreferenceKey.self) { preferences in
                if preferences != .zero && self.rect != preferences {
                    self.rect = preferences
                }
            }
    }
}

extension View {
    func childRectReader(_ rect: Binding<CGRect>) -> some View {
        modifier(ChildRectReaderModifier(rect: rect))
    }
}

struct ContentView: View {
    @State var showOptionalText = false
    @State var rect = CGRect.zero
    
    var body: some View {
        
        VStack {
            Text("\(rect.width)")
            Button {
                showOptionalText.toggle()
            } label: {
                Text("Toggle")
            }
            
            VStack {
                Text("Hello, world!")
                Text("Hello, world!")
                
                if showOptionalText {
                    Text("Hello, world!")
                }
                
                Text("Hello, world!")
            }
            .childRectReader($rect)
        }
    }
}

In the example above, the printed width is saying 0 unless I remove the if inside the inner VStack

Any idea why this happens? or any potential fixes?

CodePudding user response:

As it is used in view modifier the preference is called several times on stack so reduce is active, but it is not correct here (just assign last one overriding all previous).

As I see for this scenario the fix is (tested with Xcode 13.4 / iOS 15.5)

struct RectPreferenceKey: PreferenceKey {
    static var defaultValue = CGRect.zero

    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
      // just remove anything from here !!
    }
}

but if you wanted to handle multiple usage of this modifier for real views in same hierarchy then you have to use different model for this preference key (array, dictionary).

  • Related