I am trying to workout how to dismiss a keyboard in SwiftUI when the user taps outside a TextField. I followed some other posts that suggested using onTapGesture on a VStack and extending the View protocol to have a method to dismiss the keyboard that is called from the onTapGesture.
This is my content view:
struct ContentView: View {
#State var text = ""
var body: some View {
VStack{
Spacer()
TextField("Text", text: $text)
Text("\(text)")
Spacer()
}
.onTapGesture {
dismissKeyboard()
}
}
}
and I have extended the View protocol with:
extension View {
func dismissKeyboard() {
print("dismissing keyboard")
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
When I run the app, the keyboard is only dismissed when I tap in the area of the Text(). I would understand why this occurs if the Spacers are not there as the VStack only fills the area required by the views that it contains but I expected that the spacers would allow the onTapGesture to work for the whole screen as the VStack is pushed to the top and bottom of the screen but it still only works when tapping the Text().
Is there a reason why the onTapGesture doesn't work when I tap where the Spacers are and only works where the Text is?
You need to set a non-empty background like this:
ZStack {
Color.white.opacity(0.0000001)
VStack {
TextField("Text", text: $text)
Text("\(text)")
}
}
.onTapGesture {
dismissKeyboard()
}
SwiftUI always ignores gestures on top of things that it considers empty-space.
e.g. Color.white.opacity(0) will be considered empty-space as well and the tap gesture wont work if you do that.
Related
TLDR: The view modifier .ignoresSafeArea(.keyboard) does not appear to work when used inside a bottom sheet. Is there a workaround?
In a SwiftUI View, tapping a TextField invokes the keyboard and the Textfield then moves upwards to avoid the keyboard.
struct ContentView: View {
#State var mytext: String = "Some text"
var body: some View {
VStack {
Spacer()
TextField("abc", text: $mytext)
Spacer()
}
}
}
This keyboard avoidance behaviour can be disabled by adding the .ignoresSafeArea modifier
struct ContentView: View {
#State var mytext: String = "Some text"
var body: some View {
VStack {
Spacer()
TextField("abc", text: $mytext)
Spacer()
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
and the TextField no longer moves upwards.
If this technique is applied inside to a view in a bottom sheet it no longer works and the entire sheet is pushed up by the keyboard.
struct ContentView: View {
#State var mytext: String = "Some text"
#State var isPresented: Bool = true
var body: some View {
Color.mint
.sheet(isPresented: $isPresented) {
VStack {
Spacer()
TextField("abc", text: $mytext)
Spacer()
}
.presentationDetents( [.fraction(0.33)] )
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
}
I've tried applying .ignoresSafeArea(.keyboard, edges: .bottom) to every view thats exposed in the code with no success.
I suspect that the bug is due to the bottom sheet implementation using a UIHostingController internally. This can been seen using Xcode's Debug View Hierarchy tool.
Others have described how UIHostingController does not respect the .ignoresSafeArea(.keyboard, edges: .bottom) modifier and have developed workarounds but these are not applicable here because the UIHostingController is created internally, not explicitly in my code.
Is there any way to get the view inside the sheet to ignore the keyboard and stay put?
I'm open to any and all suggestions. Thanks!
I have a NavigationView with a toolbar that contains a ToolBarItem with a .bottomBar placement and a search field. This NavigationView contains a ScrollView with content that exceeds the screen's vertical size, which means that the bottom bar has a background, as seen below:
When the user taps the "Root View" text element they navigate to a new view, in this case, just another text displaying "Detail View". The problem, however, is that the bottom toolbar's background remains in the screen instead of vanishing as expected. See the screenshot below:
This behavior is not seen if I remove the search bar or shrink the ScrollView's height to fit the vertical dimension of the device. I tried googling this issue to see if it was a known bug with a workaround but maybe I'm not searching the right keywords. How can I fix this issue?
Please see the bare minimum to replicate the issue below:
struct BugView: View {
#State var searchPattern: String = ""
var body: some View {
NavigationView {
ScrollView {
VStack {
NavigationLink(destination: Text("Detail View")) {
Text("Root View").foregroundColor(Color.blue)
}
Spacer()
Text("Root View Bottom").foregroundColor(Color.blue)
}.frame(maxWidth: .infinity, minHeight: 1000)
}
.searchable(text: self.$searchPattern, prompt: "Search Here")
.toolbar(content: {
ToolbarItem(placement: .bottomBar) {
Text("Toolbar text")
}
})
}
}
}
Setup:
XCode Version: 13.4.1
Simulator: iPhone 13
You can use the .toolbar(.hidden, for: .bottomBar) for the destination view as shown in the code below:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Text("Destination View")
} label: {
// hiding the toolbar for the destination view
Text("Root View").toolbar(.hidden, for: .bottomBar)
}
}.toolbar {
ToolbarItem(placement: .bottomBar) {
Text("Toolbar Text")
.background {
Color.gray
}
}
}
}
}
When I develop the feature that allows users drag ScrollView to dismiss keyboard in SwiftUI, I find that if you drag ScrollView as the keyboard is dismissing, the ScrollView will flicker. That will destroy the experience of the feature.
Here's the video and minimal code example:
đź“ş Video
struct ContentView: View {
#State var text:String = ""
var body: some View {
ScrollView {
Rectangle()
.frame(height: 300)
TextField("test", text: $text)
.padding()
.background(Color.gray)
.padding()
}
}
}
It's caused because the keyboard forces the view to resize.
Add
.ignoresSafeArea(.keyboard, edges: .bottom)
to the bottom of your view to solve this issue
I have the following code:
struct ButtonTapTest: View {
let items = [1, 2, 3]
var body: some View {
List {
ForEach(items, id:\.self) { item in
CellTestView()
}
}
}
}
struct CellTestView:View {
var body: some View {
VStack {
Button {
print("TOP")
} label: {
Image(systemName: "play.fill")
.font(.system(size: 40))
.foregroundColor(.red)
}
.border(.red)
Spacer()
Button {
print("BOTTOM")
} label: {
Image(systemName: "play")
.font(.system(size: 40))
.foregroundColor(.red)
}
.border(.yellow)
}
}
}
Creates the following screen:
Problem:
Both button actions get triggered in the cell regardless of where I tap on the CellTestView. I want the individual button actions to trigger separately, each only when its button gets tapped, and not when I tap outside anywhere else on the cell.
You can see in the gif below that it does not matter where I tap on the CellTestView, both button actions get trigger regardless where on the view I tap, and both "TOP" and "BOTTOM" logs trigger at the same time.
How can I fix this so the two buttons in the cell receive the tap independently and only when the tap is inside the related button?
Whenever you have multiple buttons in a list row, you need to manually set the button style to .borderless or .plain. This is because buttons “adapt” to their context.
According to the documentation:
If you create a button inside a container, like a List, the style resolves to the recommended style for buttons inside that container for that specific platform.
So when your button is in a List, its tap target extends to fill the row and you get a highlight animation. SwiftUI isn’t smart enough to stop this side effect when you have more than 2 buttons, so you need to set buttonStyle manually.
CellTestView()
.buttonStyle(.borderless)
Result:
My workaround for this problem is to wrap each button in its own VStack or HStack.
when you have a list, tapping on a list row will trigger both buttons.
Try this code to make each "button" equivalent, trigger separately.
struct CellTestView: View {
var body: some View {
VStack {
Image(systemName: "play.fill") .font(.system(size: 40)).foregroundColor(.red).border(.red)
.onTapGesture {
print("-----> playFill \(playFill)")
}
Spacer()
Image(systemName: "play") .font(.system(size: 40)).foregroundColor(.red).border(.yellow)
.onTapGesture {
print("--> play \(play)")
}
}
}
}
I have a NavigationView with a NavigationButton inside of it, but I cannot get the NavigationButton to be at the top of the screen and still be able to be pressed, even though the navigation bar is hidden.
This code:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
NavigationButton(destination: Text("Button Clicked")) {
Text("Hello World")
.background(Color.yellow)
}
Spacer()
}
}
.navigationBarHidden(true)
}
}
Looks like , but I want it to look like .
I've tried adding a negative padding to the top of the VStack (with .padding([.top], -95), and it visually works, but then I can't interact with the button by tapping it (I think it is behind the hidden navigation bar). I've tried setting the VStack's zIndex to 10000 to solve that, but it still didn't work. Is there a way for me to move the button up to the top while still making sure that the button recognizes when it is being tapped?
Add a navigationBarTitle before hiding your navigation bar:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
NavigationButton(destination: Text("Button Clicked")) {
Text("Hello World")
.background(Color.yellow)
}
Spacer()
}
.navigationBarTitle(Text("Title")) // Add this line
.navigationBarHidden(true)
}
}
Add this modifier to your NavigationView edgesIgnoringSafeArea(.top).