skip to Main Content

I am trying to get the size of a ScrollView’s content on the initial render of the View, but it is returning 0.0 instead of the actual size. I created a ChildSizeReader View following this post to try to achieve this, and the code looks like this:

ChildSizeReader:

struct ChildSizeReader<Content: View>: View {
    @Binding var size: CGSize

    let content: () -> Content

    var body: some View {
        ZStack {
            content()
                .background(
                    GeometryReader { proxy in
                        Color.clear
                            .preference(key: SizePreferenceKey.self, value: proxy.size)
                    }
                )
        }
        .onPreferenceChange(SizePreferenceKey.self) { sizeValue in
            size = sizeValue
        }
    }
}

struct SizePreferenceKey: PreferenceKey {
    typealias Value = CGSize
    static var defaultValue: Value = .zero

    static func reduce(value _: inout Value, nextValue: () -> Value) {
        _ = nextValue()
    }
}

Use of ChildSizeReader:

@State private var scrollViewSize: CGSize = .zero

...

ScrollViewReader { proxy in
    ScrollView(.vertical) {
        ChildSizeReader(size: $scrollViewSize) {
            FeatureSet(features: features)
        }
    }
}

FeatureSet:

struct FeatureSet: View {
    var features: [Feature]

    var body: some View {
        LazyVStack {
            ForEach(Array(features.enumerated()), id: .element.title) { index, feature in
                FeatureRow(title: feature.title, value: feature.value)
                    .id(feature.title)
                
                if index != features.count - 1 {
                    Divider()
                        .frame(height: 1)
                        .overlay(Color(hex: 0x808080, opacity: 0.2))
                }
            }
        }
    }
}

I did also try wrapping Color.clear.preference in the conditional statement if proxy.size != .zero such as seen in this forum, but the size was still 0.0 when assigned to size.

But, the moment that I start scrolling, the size is set to the expected value.

Are there any ideas on how to retrieve the correct size of the ScrollView on the initial render? Thank you!

2

Answers


  1. The implementation of the preference key is incorrect. You never assigned anything to value in reduce, so it remains at the default value of .zero.

    Here is a correct implementation:

    struct SizePreferenceKey: PreferenceKey {
        static let defaultValue: CGSize? = nil
    
        static func reduce(value: inout CGSize?, nextValue: () -> Value) {
            guard let next = nextValue() else { return }
            value = next
        }
    }
    

    Note that I changed the value of the preference to be optional, so as to satisfy the invariant documented here. Namely, reduce(value: &x, nextValue: {defaultValue}) does not change the meaning of x.

    (Since Color.clear does not have any siblings, this is probably not necessary, but it’s good to follow the documentation.)

    In onPreferenceChange, unwrap the optional:

    .onPreferenceChange(SizePreferenceKey.self) { sizeValue in
        guard let sizeValue else { return }
        size = sizeValue
    }
    
    Login or Signup to reply.
  2. I would suggest simplifying your ChildSizeReader:

    • Use an .onChange handler to detect when the size of the GeometryProxy changes.

    • Supply initial: true to the .onChange handler, so that it also reads the size on initial show (this is the same as having an .onAppear callback doing the same thing).

    • A PreferenceKey is not needed.

    • The ZStack is not needed either.

    struct ChildSizeReader<Content: View>: View {
        @Binding var size: CGSize
        let content: () -> Content
    
        var body: some View {
            content()
                .background {
                    GeometryReader { proxy in
                        Color.clear
                            .onChange(of: proxy.size, initial: true) { oldVal, newVal in
                                size = newVal
                            }
                    }
                }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search