Keeping text fields above the keyboard in UIKit and SwiftUI
How the software keyboard interacts with your layout, and the modern tools for it: UIKeyboardLayoutGuide in UIKit, the keyboard safe area in SwiftUI, and @FocusState, onSubmit and scrollDismissesKeyboard for focus and dismissal.
Few things make an app feel broken faster than a keyboard that covers the field you are typing into. A login form with the "Sign in" button at the bottom, a chat input bar, a long registration form: as soon as the keyboard slides up, part of the screen disappears behind it. On iPad a floating keyboard can cover a different part of the screen each time.
UIKit and SwiftUI both have built-in tools for this today, and they replace a lot of hand-written code. This article explains how the keyboard relates to your layout, how UIKeyboardLayoutGuide and SwiftUI's keyboard safe area work, how to handle focus and dismissal, and the pitfalls that still catch people.
How the keyboard affects your layout
The software keyboard is not part of your view hierarchy. It slides in over your window, and if a text field or a "Continue" button sits in the bottom part of the screen, the keyboard covers it. The system will not rearrange your layout unless you tell it how.
Before iOS 15, telling it how meant the same ritual: observe keyboardWillShow / keyboardWillHide (or better, keyboardWillChangeFrameNotification), read the end frame from userInfo, convert it into your view's coordinate space, subtract the safe area, then either change a constraint constant or adjust a scroll view's contentInset, ideally while matching the keyboard's animation curve. It is easy to get at least one detail wrong: the iPad's floating keyboard, rotation, or removing the observer.
What changed in iOS 14 and iOS 15
Since iOS 15, every UIView has a keyboardLayoutGuide (a UIKeyboardLayoutGuide) that tracks the keyboard for you. Since iOS 14, SwiftUI treats the keyboard as a safe-area region (SafeAreaRegions.keyboard), so the layout system already avoids it. If your deployment target is iOS 15 or later, and in 2026 it almost certainly is, the notification-based approach is legacy code.
UIKit: UIKeyboardLayoutGuide
The idea is simple: the guide's top edge is the top of the keyboard. When the keyboard is hidden it rests at the bottom of the safe area. Pin your content to it and Auto Layout does the rest, including the animation.
import UIKit
final class LoginViewController: UIViewController {
private let emailField = UITextField()
private let loginButton = UIButton(configuration: .filled()) // iOS 15+
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
emailField.borderStyle = .roundedRect
emailField.placeholder = "Email"
loginButton.configuration?.title = "Sign in"
let stack = UIStackView(arrangedSubviews: [emailField, loginButton])
stack.axis = .vertical
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
// The whole trick: sit 16 points above the keyboard (or the safe area).
stack.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -16)
])
// iPad: also follow a floating or undocked keyboard (iOS 15+).
view.keyboardLayoutGuide.followsUndockedKeyboard = true
}
}
Two properties are worth knowing:
followsUndockedKeyboard(iOS 15+) isfalseby default. Then the guide only follows a docked keyboard. Set it totrueif you want the guide to follow a floating or undocked keyboard on iPad as well.usesBottomSafeArea(iOS 17+) controls whether the guide respects the bottom safe area while the keyboard is hidden. Set it tofalseif you want an input bar to sit flush with the bottom edge.
Scroll views: shrink the frame or adjust the inset
Forms usually live in a scroll view. There are two approaches, and I recommend the first one by default.
Pin the scroll view to the guide. The scroll view's frame ends at the keyboard, so every field is still reachable by scrolling. If you want the field being edited to scroll into view on its own, call scrollRectToVisible yourself:
final class FormViewController: UIViewController, UITextFieldDelegate {
private let scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.keyboardDismissMode = .interactive // swipe down to dismiss
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.keyboardLayoutGuide.topAnchor)
])
}
func textFieldDidBeginEditing(_ textField: UITextField) {
let rect = textField.convert(textField.bounds, to: scrollView)
scrollView.scrollRectToVisible(rect.insetBy(dx: 0, dy: -16), animated: true)
}
}
Adjust contentInset. This is the classic approach. It is still the right choice when content has to keep drawing behind the keyboard, for example a translucent chat background. Use keyboardWillChangeFrameNotification rather than separate show and hide notifications, and update verticalScrollIndicatorInsets (iOS 11.1+) together with the inset:
@objc private func keyboardWillChangeFrame(_ note: Notification) {
guard let endFrame = note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
let frameInView = view.convert(endFrame, from: view.window)
let overlap = max(0, view.bounds.maxY - frameInView.minY - view.safeAreaInsets.bottom)
scrollView.contentInset.bottom = overlap
scrollView.verticalScrollIndicatorInsets.bottom = overlap
}
If you do not have a specific reason for the inset approach, pin to the guide. It is less code and handles more edge cases.
SwiftUI: keyboard safe area and focus
On iOS 14 and later, SwiftUI already moves content out of the keyboard's way, because the keyboard is a safe-area region. Custom "move the field up" code written for iOS 13 tends to fight the framework on current systems.
What you actually need in SwiftUI is focus management. Here is a complete registration form:
import SwiftUI
struct SignUpForm: View {
enum Field: Hashable { case name, email, password }
@State private var name = ""
@State private var email = ""
@State private var password = ""
@FocusState private var focusedField: Field? // iOS 15+
var body: some View {
NavigationStack { // iOS 16+
ScrollView {
VStack(spacing: 16) {
TextField("Full name", text: $name)
.focused($focusedField, equals: .name)
.submitLabel(.next) // iOS 15+
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.focused($focusedField, equals: .email)
.submitLabel(.next)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
.submitLabel(.join)
}
.textFieldStyle(.roundedBorder)
.padding()
}
.scrollDismissesKeyboard(.interactively) // iOS 16+
.onSubmit { // iOS 15+
switch focusedField {
case .name: focusedField = .email
case .email: focusedField = .password
default:
focusedField = nil // dismisses the keyboard
register()
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) { // iOS 15+
Spacer()
Button("Done") { focusedField = nil }
}
}
.navigationTitle("Sign up")
}
}
private func register() { /* ... */ }
}
A few notes:
- Dismissing the keyboard is just
focusedField = nil. You no longer need the old trick of sendingresignFirstRespondertoUIApplication.shared, which is common in older SwiftUI examples. .onSubmitapplies to the text fields inside the view it is attached to, so one handler can manage the whole form..scrollDismissesKeyboardalso accepts.immediatelyand.never..interactivelyis the one that feels like Messages.
Opting out: ignoresSafeArea(.keyboard)
Sometimes you do not want things to move: a full-screen background image, or a layout where the keyboard should simply cover the bottom part. Apply .ignoresSafeArea(.keyboard) (iOS 14+) to those views:
ZStack {
Image("background")
.resizable()
.scaledToFill()
.ignoresSafeArea(.keyboard) // the background stays put
VStack {
Spacer()
TextField("Message", text: $draft)
.padding()
}
}
Common pitfalls
- Mixing approaches. A constraint on
keyboardLayoutGuideplus an old notification handler that also changescontentInsetmoves the content twice. When you migrate, delete the old observer. - Constraint conflicts. If a view is pinned to the keyboard guide and also has a fixed top constraint, something has to give when the keyboard appears. Give one of them a lower priority, or let a scroll view absorb the height.
- The modifier is on the wrong view. Where you attach
.ignoresSafeArea(.keyboard)matters. Put it on the container whose layout should not change, not on some leaf deep inside it, and check the result on a device. - iPad. Test the floating keyboard and a hardware keyboard. With a hardware keyboard you may only see the shortcuts bar, and hand-written frame math can easily get that case wrong.
- Old SwiftUI workarounds.
GeometryReaderplus keyboard publishers, or an "adaptive keyboard" modifier written for early SwiftUI, will usually conflict with the built-in avoidance on iOS 14 and later. Remove them before you debug anything else.
Checklist
- UIKit, iOS 15+: pin content or the scroll view's bottom to
view.keyboardLayoutGuide.topAnchor. - Set
followsUndockedKeyboard = trueif floating keyboards on iPad matter to you. On iOS 17+, checkusesBottomSafeArea. - Use
contentInsetwithkeyboardWillChangeFrameNotificationonly if content must stay visible behind the keyboard. - SwiftUI, iOS 14+: trust the keyboard safe area and use
.ignoresSafeArea(.keyboard)where you need to opt out. - Use
@FocusStateplus.submitLabel/.onSubmit(iOS 15+) for moving between fields and dismissing. - Use
.scrollDismissesKeyboard(.interactively)(iOS 16+) instead of tap-gesture hacks. - Delete the old keyboard notification code when you migrate.
Most of what used to be hand-written keyboard code is now a system feature. For new code, a single constraint or a single @FocusState usually does the job.
- ---
- baslik: "Keeping text fields above the keyboard in UIKit and SwiftUI"
- ozet: "How the software keyboard interacts with your layout, and the modern tools for it: UIKeyboardLayoutGuide in UIKit, the keyboard safe area in SwiftUI, and @FocusState, onSubmit and scrollDismissesKeyboard for focus and dismissal."
- tarih: 2026-09-05
- etiketler:
- - ios
- - uikit
- - swiftui
- kategoriler:
- - ios
- dil: en
- ---
- Few things make an app feel broken faster than a keyboard that covers the field you are typing into. A login form with the "Sign in" button at the bottom, a chat input bar, a long registration form: as soon as the keyboard slides up, part of the screen disappears behind it. On iPad a floating keyboard can cover a different part of the screen each time.
- UIKit and SwiftUI both have built-in tools for this today, and they replace a lot of hand-written code. This article explains how the keyboard relates to your layout, how `UIKeyboardLayoutGuide` and SwiftUI's keyboard safe area work, how to handle focus and dismissal, and the pitfalls that still catch people.
- ## How the keyboard affects your layout
- The software keyboard is not part of your view hierarchy. It slides in over your window, and if a text field or a "Continue" button sits in the bottom part of the screen, the keyboard covers it. The system will not rearrange your layout unless you tell it how.
- Before iOS 15, telling it how meant the same ritual: observe `keyboardWillShow` / `keyboardWillHide` (or better, `keyboardWillChangeFrameNotification`), read the end frame from `userInfo`, convert it into your view's coordinate space, subtract the safe area, then either change a constraint constant or adjust a scroll view's `contentInset`, ideally while matching the keyboard's animation curve. It is easy to get at least one detail wrong: the iPad's floating keyboard, rotation, or removing the observer.
- ## What changed in iOS 14 and iOS 15
- Since iOS 15, every `UIView` has a `keyboardLayoutGuide` (a `UIKeyboardLayoutGuide`) that tracks the keyboard for you. Since iOS 14, SwiftUI treats the keyboard as a safe-area region (`SafeAreaRegions.keyboard`), so the layout system already avoids it. If your deployment target is iOS 15 or later, and in 2026 it almost certainly is, the notification-based approach is legacy code.
- ## UIKit: UIKeyboardLayoutGuide
- The idea is simple: the guide's top edge is the top of the keyboard. When the keyboard is hidden it rests at the bottom of the safe area. Pin your content to it and Auto Layout does the rest, including the animation.
- ```swift
- import UIKit
- final class LoginViewController: UIViewController {
- private let emailField = UITextField()
- private let loginButton = UIButton(configuration: .filled()) // iOS 15+
- override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = .systemBackground
- emailField.borderStyle = .roundedRect
- emailField.placeholder = "Email"
- loginButton.configuration?.title = "Sign in"
- let stack = UIStackView(arrangedSubviews: [emailField, loginButton])
- stack.axis = .vertical
- stack.spacing = 12
- stack.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(stack)
- NSLayoutConstraint.activate([
- stack.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
- stack.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
- // The whole trick: sit 16 points above the keyboard (or the safe area).
- stack.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -16)
- ])
- // iPad: also follow a floating or undocked keyboard (iOS 15+).
- view.keyboardLayoutGuide.followsUndockedKeyboard = true
- }
- }
- ```
- Two properties are worth knowing:
- - `followsUndockedKeyboard` (iOS 15+) is `false` by default. Then the guide only follows a docked keyboard. Set it to `true` if you want the guide to follow a floating or undocked keyboard on iPad as well.
- - `usesBottomSafeArea` (iOS 17+) controls whether the guide respects the bottom safe area while the keyboard is hidden. Set it to `false` if you want an input bar to sit flush with the bottom edge.
- ### Scroll views: shrink the frame or adjust the inset
- Forms usually live in a scroll view. There are two approaches, and I recommend the first one by default.
- **Pin the scroll view to the guide.** The scroll view's frame ends at the keyboard, so every field is still reachable by scrolling. If you want the field being edited to scroll into view on its own, call `scrollRectToVisible` yourself:
- ```swift
- final class FormViewController: UIViewController, UITextFieldDelegate {
- private let scrollView = UIScrollView()
- override func viewDidLoad() {
- super.viewDidLoad()
- scrollView.translatesAutoresizingMaskIntoConstraints = false
- scrollView.keyboardDismissMode = .interactive // swipe down to dismiss
- 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.keyboardLayoutGuide.topAnchor)
- ])
- }
- func textFieldDidBeginEditing(_ textField: UITextField) {
- let rect = textField.convert(textField.bounds, to: scrollView)
- scrollView.scrollRectToVisible(rect.insetBy(dx: 0, dy: -16), animated: true)
- }
- }
- ```
- **Adjust `contentInset`.** This is the classic approach. It is still the right choice when content has to keep drawing behind the keyboard, for example a translucent chat background. Use `keyboardWillChangeFrameNotification` rather than separate show and hide notifications, and update `verticalScrollIndicatorInsets` (iOS 11.1+) together with the inset:
- ```swift
- @objc private func keyboardWillChangeFrame(_ note: Notification) {
- guard let endFrame = note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
- let frameInView = view.convert(endFrame, from: view.window)
- let overlap = max(0, view.bounds.maxY - frameInView.minY - view.safeAreaInsets.bottom)
- scrollView.contentInset.bottom = overlap
- scrollView.verticalScrollIndicatorInsets.bottom = overlap
- }
- ```
- If you do not have a specific reason for the inset approach, pin to the guide. It is less code and handles more edge cases.
- ## SwiftUI: keyboard safe area and focus
- On iOS 14 and later, SwiftUI already moves content out of the keyboard's way, because the keyboard is a safe-area region. Custom "move the field up" code written for iOS 13 tends to fight the framework on current systems.
- What you actually need in SwiftUI is focus management. Here is a complete registration form:
- ```swift
- import SwiftUI
- struct SignUpForm: View {
- enum Field: Hashable { case name, email, password }
- @State private var name = ""
- @State private var email = ""
- @State private var password = ""
- @FocusState private var focusedField: Field? // iOS 15+
- var body: some View {
- NavigationStack { // iOS 16+
- ScrollView {
- VStack(spacing: 16) {
- TextField("Full name", text: $name)
- .focused($focusedField, equals: .name)
- .submitLabel(.next) // iOS 15+
- TextField("Email", text: $email)
- .keyboardType(.emailAddress)
- .textInputAutocapitalization(.never)
- .focused($focusedField, equals: .email)
- .submitLabel(.next)
- SecureField("Password", text: $password)
- .focused($focusedField, equals: .password)
- .submitLabel(.join)
- }
- .textFieldStyle(.roundedBorder)
- .padding()
- }
- .scrollDismissesKeyboard(.interactively) // iOS 16+
- .onSubmit { // iOS 15+
- switch focusedField {
- case .name: focusedField = .email
- case .email: focusedField = .password
- default:
- focusedField = nil // dismisses the keyboard
- register()
- }
- }
- .toolbar {
- ToolbarItemGroup(placement: .keyboard) { // iOS 15+
- Spacer()
- Button("Done") { focusedField = nil }
- }
- }
- .navigationTitle("Sign up")
- }
- }
- private func register() { /* ... */ }
- }
- ```
- A few notes:
- - **Dismissing the keyboard** is just `focusedField = nil`. You no longer need the old trick of sending `resignFirstResponder` to `UIApplication.shared`, which is common in older SwiftUI examples.
- - **`.onSubmit`** applies to the text fields inside the view it is attached to, so one handler can manage the whole form.
- - **`.scrollDismissesKeyboard`** also accepts `.immediately` and `.never`. `.interactively` is the one that feels like Messages.
- ### Opting out: ignoresSafeArea(.keyboard)
- Sometimes you do *not* want things to move: a full-screen background image, or a layout where the keyboard should simply cover the bottom part. Apply `.ignoresSafeArea(.keyboard)` (iOS 14+) to those views:
- ```swift
- ZStack {
- Image("background")
- .resizable()
- .scaledToFill()
- .ignoresSafeArea(.keyboard) // the background stays put
- VStack {
- Spacer()
- TextField("Message", text: $draft)
- .padding()
- }
- }
- ```
- ## Common pitfalls
- - **Mixing approaches.** A constraint on `keyboardLayoutGuide` *plus* an old notification handler that also changes `contentInset` moves the content twice. When you migrate, delete the old observer.
- - **Constraint conflicts.** If a view is pinned to the keyboard guide and also has a fixed top constraint, something has to give when the keyboard appears. Give one of them a lower priority, or let a scroll view absorb the height.
- - **The modifier is on the wrong view.** Where you attach `.ignoresSafeArea(.keyboard)` matters. Put it on the container whose layout should not change, not on some leaf deep inside it, and check the result on a device.
- - **iPad.** Test the floating keyboard and a hardware keyboard. With a hardware keyboard you may only see the shortcuts bar, and hand-written frame math can easily get that case wrong.
- - **Old SwiftUI workarounds.** `GeometryReader` plus keyboard publishers, or an "adaptive keyboard" modifier written for early SwiftUI, will usually conflict with the built-in avoidance on iOS 14 and later. Remove them before you debug anything else.
- ## Checklist
- 1. UIKit, iOS 15+: pin content or the scroll view's bottom to `view.keyboardLayoutGuide.topAnchor`.
- 2. Set `followsUndockedKeyboard = true` if floating keyboards on iPad matter to you. On iOS 17+, check `usesBottomSafeArea`.
- 3. Use `contentInset` with `keyboardWillChangeFrameNotification` only if content must stay visible behind the keyboard.
- 4. SwiftUI, iOS 14+: trust the keyboard safe area and use `.ignoresSafeArea(.keyboard)` where you need to opt out.
- 5. Use `@FocusState` plus `.submitLabel` / `.onSubmit` (iOS 15+) for moving between fields and dismissing.
- 6. Use `.scrollDismissesKeyboard(.interactively)` (iOS 16+) instead of tap-gesture hacks.
- 7. Delete the old keyboard notification code when you migrate.
- Most of what used to be hand-written keyboard code is now a system feature. For new code, a single constraint or a single `@FocusState` usually does the job.