iosswiftswiftuiscrollview

ScrollView does not allow the view behind it to be clicked even if there is nothing there


I have a very basic view that has a background (Color.red) and some content inside a ScrollView. The view worked great until I noticed something. After selecting the text field and tappint at the background, the keyboard wasn't dismissing. Even if I clicked somewhere that had nothing visually (expect the background) it still didn't work.

struct ContentView: View {
    @State private var filter = ""

    var body: some View {
        ZStack {
            Color.red.ignoresSafeArea(.all)
                .onTapGesture {
                    // Dismiss keyboard
                }

            ScrollView {
                VStack(spacing: 8) {
                    TextField("Some Filter", text: $filter)
                        .textFieldStyle(.roundedBorder)
                    Button("clicky click") {
                        // Fetch data
                    }
                    .buttonStyle(.borderedProminent)

                    // Other data result related stuff
                }
            }
            .refreshable {
                // Fetch data
            }
        }
    }
}

So to solve it I tried doing these below:

I'm running iOS 15. And I'm looking for a solution that isn't too complicated. Thanks in advance.


Solution

  • The tap gesture probably needs to be attached to a view inside or above the ScrollView. So you could try wrapping the ScrollView with a GeometryReader. Then:

    The only issue I found with this was that the safe area insets were not filled with the background color, even when .ignoresSafeArea() was applied. So as a fallback, the same color can be applied as background to the ScrollView too. This means, the safe areas are not receptive to taps, but perhaps this is acceptable.

    struct ContentView: View {
        @State private var filter = ""
        @FocusState private var isFocused: Bool
    
        var body: some View {
            GeometryReader { proxy in
                ScrollView {
                    VStack(spacing: 8) {
                        TextField("Some Filter", text: $filter)
                            .textFieldStyle(.roundedBorder)
                            .focused($isFocused)
                        Button("clicky click") {
                            // Fetch data
                        }
                        .buttonStyle(.borderedProminent)
    
                        // Other data result related stuff
                    }
                    .frame(minWidth: proxy.size.width, minHeight: proxy.size.height, alignment: .topLeading)
                    .background {
                        Color.red
                            .ignoresSafeArea()
                            .onTapGesture {
                                isFocused = false
                            }
                    }
                }
                .background(.red)
                .refreshable {
                    // Fetch data
                }
            }
        }
    }
    

    Animation


    Instead of applying the same background to both the VStack and the ScrollView, the background to the VStack could just use Color.clear instead. This would also be a suitable approach for when the background behind the ScrollView is something more elaborate than a simple color, such as when an image or gradient is used, or when no background is required at all.

    Doing it this way, it is necessary to apply a .contentShape to the clear background, to make it receptive to taps:

    VStack(spacing: 8) {
        // ...
    }
    .frame(minWidth: proxy.size.width, minHeight: proxy.size.height, alignment: .topLeading)
    .background {
        Color.clear
            .contentShape(.rect)
            .onTapGesture {
                isFocused = false
            }
    }