Fix: Keyboard Covers UITextField in Swift and UIKit

Fix: Keyboard Covers UITextField in Swift and UIKit

Written by

in

Quick answer: In modern UIKit (iOS 15 and later), constrain the bottom of your view or input container to view.keyboardLayoutGuide.topAnchor using Auto Layout. For older iOS versions or manual layout, observe UIResponder.keyboardWillShowNotification and UIResponder.keyboardWillHideNotification to animate your view’s frame or layout constraints using the keyboard height from userInfo.

Why this happens

When a user taps into a UITextField or UITextView located in the lower half of the screen, iOS displays the software keyboard as an overlay directly above the current window. In standard UIKit view hierarchies, the view controller’s root view does not automatically adjust its position or height when the keyboard appears.

Because the keyboard renders in its own top-level window (UIRemoteKeyboardWindow), it does not push the underlying view hierarchy upward. The input element retains its original screen coordinates, resulting in the keyboard obscuring the text field and preventing the user from seeing what they type.

Resolving this requires either anchoring your views to Apple’s built-in keyboard layout guide or intercepting system keyboard notifications to update Auto Layout constraints or frame offsets dynamically.

How to fix it

1. Constrain views to keyboardLayoutGuide (iOS 15+)

Starting in iOS 15, UIKit provides UIKeyboardLayoutGuide. This is the cleanest and most reliable way to position views above the keyboard because it automatically tracks keyboard height, docking status, hardware keyboards, and dismissal animations without manual notification handling.

Anchor the bottom of your text field or containing stack view to view.keyboardLayoutGuide.topAnchor:

import UIKit

class LoginViewController: UIViewController {
    private let textField: UITextField = {
        let field = UITextField()
        field.placeholder = "Enter password"
        field.borderStyle = .roundedRect
        field.translatesAutoresizingMaskIntoConstraints = false
        return field
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        view.addSubview(textField)

        NSLayoutConstraint.activate([
            textField.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
            textField.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
            // Pin the bottom of the field to the top of the keyboard guide
            textField.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -16)
        ])
    }
}

When the keyboard is dismissed, view.keyboardLayoutGuide.topAnchor aligns with the bottom of the screen (or the bottom safe area). When the keyboard opens, the anchor tracks the keyboard frame in real time, animating your text field upward automatically.

2. Observe keyboard notifications and update constraints

If you need backward compatibility with iOS 14 or want fine-grained programmatic control over the animation, observe system notifications via NotificationCenter. Instead of shifting the entire view.frame.origin.y (which can break navigation bars and modal presentations), update a bottom Auto Layout constraint.

First, retain a reference to your bottom constraint:

import UIKit

class DynamicFormViewController: UIViewController {
    private var bottomConstraint: NSLayoutConstraint?

    private let submitButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Submit", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        view.addSubview(submitButton)

        let bottom = submitButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16)
        self.bottomConstraint = bottom

        NSLayoutConstraint.activate([
            submitButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            bottom
        ])

        setupKeyboardObservers()
    }

    private func setupKeyboardObservers() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleKeyboardNotification),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleKeyboardNotification),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
    }

    @objc private func handleKeyboardNotification(notification: Notification) {
        guard let userInfo = notification.userInfo,
              let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
              let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double,
              let curveValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else {
            return
        }

        let isHiding = notification.name == UIResponder.keyboardWillHideNotification
        let bottomOffset: CGFloat

        if isHiding {
            bottomOffset = -16
        } else {
            // Subtract safe area inset so the view does not double-count the bottom inset
            let safeAreaBottom = view.safeAreaInsets.bottom
            bottomOffset = -(keyboardFrame.height - safeAreaBottom + 16)
        }

        bottomConstraint?.constant = bottomOffset

        let options = UIView.AnimationOptions(rawValue: curveValue << 16)
        UIView.animate(withDuration: duration, delay: 0, options: options) {
            self.view.layoutIfNeeded()
        }
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

3. Adjust UIScrollView content insets for long forms

When a screen contains multiple text fields, shifting the entire view often causes top elements (like headers, navigation bars, or upper fields) to slide out of sight. The standard Apple HIG solution is embedding all inputs inside a UIScrollView and adjusting its contentInset.

import UIKit

class ScrollFormViewController: UIViewController {
    private let scrollView = UIScrollView()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupLayout()
        registerForKeyboardEvents()
    }

    private func setupLayout() {
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(scrollView)

        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }

    private func registerForKeyboardEvents() {
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    }

    @objc private func keyboardWillChangeFrame(notification: Notification) {
        guard let userInfo = notification.userInfo,
              let keyboardEndFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
            return
        }

        let keyboardFrameInView = view.convert(keyboardEndFrame, from: nil)
        let isDismissed = keyboardFrameInView.origin.y >= view.bounds.height

        if isDismissed {
            scrollView.contentInset = .zero
            scrollView.verticalScrollIndicatorInsets = .zero
        } else {
            let coveredHeight = view.bounds.height - keyboardFrameInView.origin.y
            let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: coveredHeight, right: 0)
            scrollView.contentInset = contentInsets
            scrollView.verticalScrollIndicatorInsets = contentInsets
        }
    }
}

This approach ensures that every text field remains accessible via scrolling without clipping navigation bars or search headers.

How to prevent it

  • Prefer view.keyboardLayoutGuide for single-field screens or bottom toolbars on iOS 15 and above.
  • Embed forms and multi-input interfaces in a UIScrollView from the start so insets can expand as needed.
  • Convert coordinate spaces with view.convert(_:from:) when calculating keyboard overlaps, ensuring compatibility with iPad multitasking and split-screen modes.
  • Account for view.safeAreaInsets.bottom when computing height offsets so your view does not jump higher than the actual keyboard frame.
  • Always extract the animation curve and duration from notification.userInfo to synchronize your view transition with the keyboard slide.

FAQ

Why does altering self.view.frame.origin.y mess up my navigation bar?

Modifying the root view’s frame origin shifts the entire coordinate space upward, often pushing UINavigationBar off the top of the window and creating blank spaces under presentation contexts. Modifying Auto Layout constraints or scroll view insets keeps the root view intact.

How do I dismiss the keyboard when tapping outside the text field?

Attach a UITapGestureRecognizer to your root view with cancelsTouchesInView = false and invoke view.endEditing(true) in the action selector.

Does SwiftUI suffer from this same issue?

SwiftUI automatically accounts for the keyboard within Form, List, and standard layouts by adjusting safe area insets. If you need a view to ignore the keyboard in SwiftUI, you must explicitly declare .ignoresSafeArea(.keyboard, edges: .bottom).

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.