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 tried to add .allowsHitTesting(false)
but it completely bricked the scroll view.
(Expected)
I tried to add a invisible view to the scroll view that did the same thing. But it became tiny. I tried to force it to be big but the scroll view became scrollable down to nothingness.
I tried to add it as a dynamic footer (the dynamicly streching ones). This worked, but it was way too costly for what it was doing. And it way too overcomplicated my view.
I'm running iOS 15. And I'm looking for a solution that isn't too complicated. Thanks in advance.
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:
VStack
to the size delivered by the GeometryReader
.VStack
.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
}
}
}
}
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
}
}