1

I want to use a ScrollView outside of a VStack, so that my content is scrollable if the VStack expands beyond screen size. Now I want to use GeometryReader within the VStack and it causes problems, which I can only solve by setting the GeometryReader frame, which does not really help me given that I use the reader to define the view size.

Here is the code without a ScrollView and it works nicely:

struct MyExampleView: View {
  var body: some View {
    VStack {
        Text("Top Label")
            .background(Color.red)
        
        GeometryReader { reader in
            Text("Custom Sized Label")
                .frame(width: reader.size.width, height: reader.size.width * 0.5)
                .background(Color.green)
        }
        
        Text("Bottom Label")
            .background(Color.blue)
    }
    .background(Color.yellow)
  }
}

This results in the following image:

without scroll view

The custom sized label should be full width, but half the width for height. Now if I wrap the same code in a ScrollView, this happens:

with scroll view

Not just did everything get smaller, but the height of the Custom Sized Label is somehow ignored. If I set the height of the GeometryReader, I can adjust that behaviour, but I want to GeometryReader to grow as large as its content. How can I achieve this?

Thanks

Janosch Hübner
  • 1,314
  • 17
  • 34

1 Answers1

2

It should be understood that GeometryReader is not a magic tool, it just reads available space in current context parent, but... ScrollView does not have own available space, it is zero, because it determines needed space from internal content... so using GeometryReader here you have got cycle - child asks parent for size, but parent expects size from child... SwiftUI renderer somehow resolves this (finding minimal known sizes), just to not crash.

Here is possible solution for your scenario - the appropriate instrument here is view preferences. Prepared & tested with Xcode 12 / iOS 14.

demo

struct DemoLayout_Previews: PreviewProvider {
    static var previews: some View {
        Group {
          MyExampleView()
          ScrollView { MyExampleView() }
        }
    }
}

struct MyExampleView: View {
    @State private var height = CGFloat.zero
    var body: some View {
        VStack {
            Text("Top Label")
                .background(Color.red)
            
            Text("Custom Sized Label")
                .frame(maxWidth: .infinity)
                .background(GeometryReader {
                    // store half of current width (which is screen-wide)
                    // in preference
                    Color.clear
                        .preference(key: ViewHeightKey.self, 
                            value: $0.frame(in: .local).size.width / 2.0)
                })
                .onPreferenceChange(ViewHeightKey.self) {
                    // read value from preference in state
                    self.height = $0
                }
                .frame(height: height) // apply from stored state
                .background(Color.green)
            
            Text("Bottom Label")
                .background(Color.blue)
        }
        .background(Color.yellow)
    }
}

struct ViewHeightKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}

Note: ... and don't use GeometryReader if you are not sure about context in which your view is.

Asperi
  • 123,447
  • 8
  • 131
  • 245
  • Thanks for that! Where is the preference value set exactly? If I set the initial value to 0, my view height is 0. If I set it to 10, it is 10. – Janosch Hübner Oct 10 '20 at 13:31
  • See added more comments, view preferences pass values from child to parent in same render cycle. – Asperi Oct 10 '20 at 13:39
  • Thanks! Weird that it does not work for me like that. It seems onPreferenceChanged is not called. I will work on that. – Janosch Hübner Oct 10 '20 at 13:50