You are currently viewing Automatic Keyboard Avoidance for UIKit

Automatic Keyboard Avoidance for UIKit

In iOS 14, Apple shows their love to SwiftUI by giving it automatic keyboard avoidance, it is turned on by default, meaning all your SwiftUI views can get this awesome feature automatically once you build your apps for iOS 14.

Unfortunately, for whatever reason, Apple does not make this available for UIKit despite the fact that it is one of the most requested features by developers around the world.

In this article, I would like to show you how to enable automatic keyboard avoidance in a view controller using a trick that I recently discovered. The best part is that this simple trick does not require any third party library.

Without further ado, let’s get right into it!


The Sample App

For demonstration purposes, I have created a view controller named TextFieldViewController that is filled up by a stack view. Within the stack view, there is a yellow view that takes up the top half of the view controller, and another blue view that takes up the bottom half of the view controller. Within the bottom blue view, I added a UITextField in order to trigger the keyboard.

Here’s the full implementation of TextFieldViewController:

import UIKit

class TextFieldViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        // Configure stack view and make it fill up the entire view controller
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.alignment = .fill
        stackView.distribution = .fillEqually
        view.addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
            stackView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
            stackView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
        ])

        // Add yellow view to top half of stack view
        let topYellowView = UIView()
        topYellowView.backgroundColor = .systemYellow
        stackView.addArrangedSubview(topYellowView)

        // Add blue view to bottom half of stack view
        let bottomBlueView = UIView()
        bottomBlueView.backgroundColor = .systemBlue
        stackView.addArrangedSubview(bottomBlueView)

        // Add text field at center of bottom blue view
        let textField = UITextField()
        textField.borderStyle = .roundedRect
        textField.translatesAutoresizingMaskIntoConstraints = false
        bottomBlueView.addSubview(textField)
        NSLayoutConstraint.activate([
            textField.widthAnchor.constraint(equalToConstant: 150),
            textField.heightAnchor.constraint(equalToConstant: 44),
            textField.centerXAnchor.constraint(equalTo: bottomBlueView.centerXAnchor),
            textField.centerYAnchor.constraint(equalTo: bottomBlueView.centerYAnchor),
        ])
        textField.delegate = self
    }
}

extension UIViewController: UITextFieldDelegate {
    
    public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        // Dismiss keyboard when tap on "return" button
        textField.resignFirstResponder()
        return false
    }
}

In UIKit, when we select the text field, the keyboard will block the text field. This is a very bad user experience because users are not able to see the text field content while typing.

Without automatic keyboard avoidance in UIKit in iOS 14
Without automatic keyboard avoidance in UIKit

Instead, what we want the system to do is to automatically shift the text field up, so that the keyboard will not block the text field.

Automatic keyboard avoidance enabled in UIKit in iOS 14
Automatic keyboard avoidance enabled in UIKit

The Concept

Recently I discovered that automatic keyboard avoidance is not limited to SwiftUI views only, it is also available for structs that conform to the UIViewControllerRepresentable protocol. I guess this is because, in the system context, an UIViewControllerRepresentable instance is treated as a SwiftUI view as well.

If that is the case, making automatic keyboard avoidance available for UIKit is pretty straightforward. All we need to do is wrap the UIViewControllerRepresentable instance in a UIHostingController and present it.

With that in mind, let’s dive into the code.


The Implementation

First, we need to implement a struct that conforms to the UIViewControllerRepresentable protocol. This struct basically converts the TextFieldViewController we created earlier into a SwiftUI view. Let’s name it TextFieldView.

import SwiftUI

struct TextFieldView: UIViewControllerRepresentable {
    
    func makeUIViewController(context: Context) -> TextFieldViewController {
        let viewController = TextFieldViewController()
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: TextFieldViewController, context: Context) {
        // Empty implementation
    }
}

With that in place, we can now wrap the TextFieldView in a UIHostingController and present it as how we usually present a view controller.

let textFieldView = TextFieldView()
let viewController = UIHostingController(rootView: textFieldView)
self.present(viewController, animated: true, completion: nil)

The following animated GIF showcase the final result of our implementation:

Default automatic keyboard avoidance behavior in UIKit in iOS 14
Default automatic keyboard avoidance behavior in UIKit

That’s it! We have successfully enabled automatic keyboard avoidance in our TextFieldViewController. We can now have a feel for how it works on UIKit.

It seems like it works similarly to SwiftUI: instead of overlapping with the view controller’s container view, the keyboard will now push the bottom safe area layout guide up, reducing the height of the container view, thus compressing both the yellow and the blue view.

This behavior might seem OK in our sample app (with only 2 subviews and a text field). But imagine you have a complicated view with multiple text fields and buttons, reducing its height in such a scale will definitely break its entire UI.

Therefore, the behavior that we are looking for is: when the keyboard is shown, shift the text view up so that it is not blocked by the keyboard, while maintaining the container view’s dimension.

Fortunately, this behaviour can be easily achieved by using a scroll view.


Scroll View to the Rescue

The trick here is to wrap everything inside the view controller’s container view in a scroll view. With that, when the keyboard is shown, the scroll view frame height will be compressed, whereas the scroll view content height will remain the same.

For our TextFieldViewController, what needs to be done is to fill the container view with a scroll view, and move the stack view into the scroll view.

override func viewDidLoad() {
 
    // ...
    // ...
    
    // Add a scroll view that fill up the entire view controller
    let scrollView = UIScrollView()
    view.addSubview(scrollView)
    scrollView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        scrollView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
        scrollView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
        scrollView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
        scrollView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
    ])

    // Add stack view to scroll view
    let stackView = UIStackView()
    stackView.axis = .vertical
    stackView.alignment = .fill
    stackView.distribution = .fillEqually
    stackView.translatesAutoresizingMaskIntoConstraints = false
    scrollView.addSubview(stackView)
    NSLayoutConstraint.activate([
        stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
        stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
        stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
        stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
        stackView.heightAnchor.constraint(equalToConstant: 753), // Explicitly setting the stack view height
        stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
    ])
 
    // ...
    // ...
}

From the code above, do note that we have to explicitly set the stack view height, so that the scroll view can determine its content height during runtime.

Here’s what we get after the changes:

Automatic keyboard avoidance using scroll view in UIKit in iOS 14
Automatic keyboard avoidance using scroll view

Disabling Automatic Keyboard Avoidance

Even though automatic keyboard avoidance is a very nice feature to have, there are still situations where we might want to handle it manually. If that is the case, we can leverage the ignoresSafeArea(_:edges:) view modifier to turn it off like so:

// Create a `TextFieldView` that ignore keyboard safe area
let textFieldView = TextFieldView().ignoresSafeArea(.keyboard)
let viewController = UIHostingController(rootView: textFieldView)
self.present(viewController, animated: true, completion: nil)

Do note that ignoresSafeArea(_:edges:) is a SwiftUI view modifier, as a result, it won’t work on view controllers.


Wrapping Up

I find this trick that enables automatic keyboard avoidance very useful and I like it quite a lot. On the other hand, it does feel a little bit hacky to me. What do you think? If you manage to spot any unforeseen issues with this trick, please let me know.

I hope you enjoy reading this article, if you do, feel free to check out my other iOS development related articles here. You can also follow me on Twitter, and subscribe to my monthly newsletter.

Thanks for reading. 👨🏻‍💻


References


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.