In my iOS app, I have a VStack that consists of a view with a List and a view with a TextInput:
var body: some View {
NavigationView {
ScrollViewReader { scrollProxy in
VStack() {
NoteListView(notes: notes)
.onTapGesture { hideKeyboard() }
NoteInputView(scrollProxy: scrollProxy)
}
}
.navigationBarTitle(Text("Notes"))
}
}
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
On my NoteListView, I have attached a .swipeAction to delete Notes:
List(notes) { note in
NotePreview(note: note)
.swipeActions(edge: .leading, allowsFullSwipe: true) {
Button(role: .destructive) {
delete(note)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
}
While this hides the keyboard successfully, it also prevents the tap gesture to be recognized by the delete button on the swipe action. Using .simultaneousGesture does not fix this issue, the only way to get the delete button to work when tapping is to remove any tap gestures attached to parent views—adding .highPriorityGesture to the button does not fire, either. This seems like a SwiftUI bug to me.
Attempted Workaround—works unpredictably?
Instead of using a VStack, I decided to move to a ZStack that fills the entire screen whenever the keyboard is showing. When the keyboard is showing, a Spacer captures tap events. When the keyboard is not showing, not taps should be captured:
var body: some View {
NavigationView {
ScrollViewReader { scrollProxy in
ZStack() {
NoteListView(notes: notes)
NoteInputView(scrollProxy: scrollProxy)
}
}
.navigationBarTitle(Text("Notes"))
}
}
In my NoteInputView I now have @FocusState to track whether the TextInput has focus:
struct NoteInputView: View {
...
@FocusState var focusInputField: Bool
var body: some View {
ZStack(alignment: .bottom) {
if(focusInputField) {
Spacer()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.simultaneousGesture(TapGesture().onEnded({ _ in
focusInputField = false
print("I'm still standing yeah yeah yeah")
print(focusInputField)
})).onTap
} else {
Spacer()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
HStack() {
if(focusInputField) {
Button("", systemImage: "keyboard.chevron.compact.down") {
focusInputField = false
}
}
TextField("Enter a quick note...", text: $newNoteContent, axis: .vertical)
.lineLimit(1...5)
}
}
}
}
However, this works unreliably—sometimes, the Spacer capturing the tap will still print I'm still standing yeah yeah yeah despite focusInputField being false. So far, I have not been able to reliably reproduce when the Spacer remains or when it disappears.
Would be glad to hear other workarounds, or feedback on why this might be working unreliably.