iosxcodeswiftui

SwiftUI GeometryReader returning 0.0 for ScrollView content size on initial render


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!


Solution

  • I would suggest simplifying your ChildSizeReader:

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

    An alternative way to implement the size reader is as a ViewModifier. I added a new answer to the post you referenced, to show how it can work this way.