Dismissing the Keyboard in SwiftUI 2.0

Before you break your actual keyboard, let me show you the way.

Jonathancbadger
The Startup
5 min readNov 5, 2020

--

Photo by Justus Menke on Unsplash

SwiftUI, Apple’s declarative framework for rapid user interface development, has continued to mature as a useful alternative to UIKit with a 2.0 release at WWDC this year. As a newbie to the framework, I enjoy the instant feedback that the canvas preview provides with each code modification and am thoroughly impressed by the elegance and simplicity of SwiftUI’s design. That being said, I have run into odd behaviors and challenging problems that make me want to tear my hair out at times. One such issue, which at first seems trivial, is how to dismiss the keyboard when a user taps an area on the screen that is outside the bounds of a TextField or TextEditor view. Here, I will discuss a few of the common issues surrounding keyboard dismissal and provide two solutions and workarounds that I have found after an embarrassing amount of googling and reading on StackOverflow.

Notes: 1. A demo iOS app that accompanies this post is available on GitHub. This article was written using XCode 12.1, SwiftUI 2.0, and compiled for iOS 14.1.

The crux of the problem

If you have already spent an hour trying to figure out how to dismiss the keyboard in SwiftUI you are not alone. SwiftUI doesn’t currently expose a mechanism for manual keyboard dismissal from within its own framework, so we have to fall back on UIKit. In UIKit, views that utilize the keyboard such as UITextField and UITextView can be dismissed by calling resignFirstResponder on the view itself, or endEditing on one of its parent views. Code in a simple view controller might look like this:

Here I have implemented both options for keyboard dismissal on a UITextField named textField. To add support for keyboard dismissal on taps outside of textField we add a tap gesture to the view controller's view in viewDidLoad. When an outside tap is recognized dismissKeyboard gets called and the view controller's view calls endEditing. This in turn leads to textField calling resignFirstResponder, dismissing the keyboard. As a second option, we also have a button that when pressed calls dismissKeyboardFrom(view:). Here we pass in textField and directly call resignFirstResponder to dismiss the keyboard.

Side note: Both of these methods utilize the responder chain. If you are unfamiliar with the responder chain or need a refresher on UIResponder, UIEvent, UIControl, and how touch events and other gestures are handled in UIKit I would highly recommend reading this excellent article written by Bruno Rocha from Better Programming on Medium. The official docs on the responder chain from Apple are also extremely helpful and well written.

Solutions

So how do we bring this behavior into SwiftUI?

Option 1

The first solution I found comes from HackingWithSwift. Here, they suggest extending the View protocol and utilizing the responder chain to send out a resignFirstResponder message. The extension looks like this:

This solution works beautifully if you attach the function to a submit button as they do in their example:

One would have hoped we could extend this idea by adding a tap gesture view modifier to a VStack or Form to get the keyboard to dismiss on an outside tap as in:

Adding onTapGesture to a VStack (pink).

But this won’t work as expected. The first problem is that views created in SwiftUI typically only cover a portion of the screen, unlike the view associated with a view controller, which usually covers the entire screen. In the example on the right I have colored the background of the VStack pink and as you can see, adding dismissKeyboard to the VStack will still leave a lot of non-interactive whitespace, which may be frustrating to users as they have to find just the right spot to get the behavior they expect.

The second problem is that when you try and use dismissKeyboard in a Form it may break some of the other interactive controls in the view. For example, if we have something like:

Our keyboard will show and dismiss, but we can no longer interact with the picker view or press the submit button. Clearly, we need to try something else.

Option 2

The second fix I am going to show you adds keyboard dismissal app-wide and fixes the issues we observed in the last section. The original solution and other ideas from StackOverflow can be found here. In short, you can add a tap gesture recognizer to the underlying UIWindow of the app that acts in a similar fashion to how we set up keyboard dismissal in UIKit by adding a tap gesture to the view of our view controller. To begin, extend UIApplication with a new method:

The addTapGestureRecognizer method creates a UITapGestureRecognizer, tapGesture, that will target the app's window and call UIView.endEditing to dismiss the keyboard. Since we still want the other parts of the user experience to function as normal, we set cancelsTouchesInView to false. We set the UIApplication as the delegate for tapGesture (you'll see why in a minute) and add the gesture to the first window in the windows array of UIApplication.

Simultaneous gestures, such as a double tap, are important for selecting text in a TextView. If we leave things as they stand, by default our new tap gesture will get called for both double and single taps, meaning when we try and double tap to select text, it will dismiss the keyboard. Oops! To fix this we can implement the gestureRecognizer(_ gestureRecognizer: , shouldRecognizeSimultaneouslyWith otherGestureRecognizer: ) delegate method of UIGestureRecognizerDelegate and return false.

The last step is to add the gesture to the app window when the application launches, which can be done like this if you are using the new SwiftUI app lifecycle:

Or, if you are still using the UIKit lifecycle you can instead skip extending UIApplication and set up your SceneDelegate similar to:

The final results give us the user interaction we were looking for. Touching anywhere outside of the TextField dismisses the keyboard and all of our other controls work as expected. Success!

Final Thoughts

The second solution appears to give us back the behavior we are interested in, but I do have a few concerns that should be mentioned. First, I haven’t played much with apps that support multiple windows for iPads, but I suspect this current fix may not work as expected in a multi-window scenario. Second, I am a bit concerned future updates to SwiftUI and its inner workings may break this current fix, or worse, introduce new bugs that are hard to diagnose. My hope is that in the next update to SwiftUI Apple will give us an official solution to manual keyboard dismissal. At least we have a solution that can keep us productive while we wait.

Originally published at https://www.dabblingbadger.com on November 5, 2020.

--

--

The Startup
The Startup

Published in The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +772K followers.

Jonathancbadger
Jonathancbadger

Written by Jonathancbadger

Pharmacist, data scientist, Apple developer, and maker.