iosswiftui

swiftui TextField() cannot be tapped in certain ordering of containers


I've run into a strange behavior and hoping someone can help me understand why it is happening so I can avoid problems like this in the future. I've got a complicated View but I've stripped it alllll the way down as far as I can while still demonstrating the problem/bug/whatever, and I've included that below.

The real view has a ZStack with a VStack and a Grid, etc, but I've stripped it all out until I got down to this.

// stripped down version of the view
struct DebugWTF: View {
        
    @State var debugstring: String = ""
    
    var body: some View {
        ScrollView {
            GeometryReader { geometry in
                TextField("placeholder_eg", text: $debugstring)
            }
        }
    }
}

I've discovered that if I change the ordering of the containers, then the TextField() starts working normally.

var body: some View {
    ScrollView {
        GeometryReader { geometry in 
            TextField(...) // is NOT tappable
        }
    }
}

then my TextField() cannot be tapped on. But it DOES work if I swap those:

var body: some View {
    GeometryReader { geometry in 
        ScrollView {        
            TextField(...) // IS tappable
        }
    }
}

When that worked, I thought maybe the ScrollView was behaving strangely if its direct child was the GeometryReader, but even if I insert a ZStack between them, the TextField() is not tappable again:

var body: some View {
    ScrollView {
        ZStack {
            GeometryReader { geometry in 
                TextField(...) // is NOT tappable
            }
        }
    }
}

Can anyone explain why the ordering of these is causing a problem? I'd like to understand what is fundamentally happening so that I can understand how to avoid such problems in the future.


Solution

  • A GeometryReader will normally fill all the available space, and that is how it can read the size of the container. As the documentation says, the content of a GeometryReader is a function of the GeometryReader's own size.

    However, in a ScrollView, there is no such thing as the "available space". Space is always available! In this case, the GeometryReader will resize to its ideal width or ideal height (depending on the axis of the scroll view). It is as if you have applied .fixedSize to the GeometryReader.

    "Ideal size" doesn't make sense for GeometryReader since it is designed to fill as much space as possible, but all SwiftUI views need to have an ideal size. It appears that the convention in SwiftUI is to use the arbitrarily chosen value of "10".

    Try running this code. The actual frame of the GeometryReader is shown in yellow, and you can see that the second Text shows that its height is 10.

    ScrollView {
        GeometryReader { geo in
            HStack {
                Text(geo.size.width, format: .number)
                Text(geo.size.height, format: .number)
            }
        }
        .background(.yellow)
    }
    

    Your TextField is most likely more than 10pts tall, so the TextField's frame exceeds the frame of the GeometryReader. The parts of the TextField that exceeds the GeometryReader will not be tappable. If the GeometryReader is .clipped, you will barely see the TextField at all.


    If you just want to measure the container size of the ScrollView, then putting the GeometryReader outside of the ScrollView is correct.

    If you want to measure the content size of the ScrollView, then you can put the GeometryReader as an .overlay or .background of the scroll view content.

    ScrollView {
        SomeContentView()
            .background {
                GeometryReader { geo in ... }
            }
    }