Write, review, or improve UIKit code following best practices for view controller lifecycle, Auto Layout, collection views, navigation, animation, memory management, and modern iOS 18–26 APIs. Use when building new UIKit features, refactoring existing views or view controllers, reviewing code quality, adopting modern UIKit patterns (diffable data sources, compositional layout, cell configuration), or bridging UIKit with SwiftUI. Does not cover SwiftUI-only code.
96
100%
Does it follow best practices?
Impact
96%
1.23xAverage score across 9 eval scenarios
Passed
No known issues
UIKeyboardLayoutGuide (iOS 15+) is the definitive modern API for keyboard-aware layouts, replacing hundreds of lines of notification-based boilerplate with a single Auto Layout constraint. This guide covers seven key areas of UIKit keyboard management — from the modern layout guide through legacy notification fallbacks to the newest iOS 26 strongly typed notification APIs — with production-ready code patterns for each.
The keyboard handling landscape shifted fundamentally at WWDC 2021 with UIKeyboardLayoutGuide, matured through iOS 17 with usesBottomSafeArea and keyboardDismissPadding, and gained developer ergonomics in iOS 26 with strongly typed notification messages. Understanding all layers remains critical: the layout guide for new projects, notifications for backward compatibility, and scroll view inset mechanics for both.
UIKeyboardLayoutGuide is a subclass of UITrackingLayoutGuide available on every UIView via the view.keyboardLayoutGuide property since iOS 15.0+. It represents the space the keyboard occupies and exposes standard Auto Layout anchors — topAnchor, bottomAnchor, leadingAnchor, trailingAnchor, heightAnchor, widthAnchor, and center anchors.
Three behaviors make it powerful. First, constraints animate with the keyboard automatically — no UIView.animate calls needed. Second, when the keyboard is hidden, the guide's topAnchor aligns with safeAreaLayoutGuide.bottomAnchor, so content rests at the safe area boundary by default. Third, it tracks all keyboard height changes (emoji keyboard, QuickType bar toggle) without additional code.
override func viewDidLoad() {
super.viewDidLoad()
// Single constraint replaces all notification-based keyboard handling
textView.bottomAnchor.constraint(
equalTo: view.keyboardLayoutGuide.topAnchor
).isActive = true
}NSLayoutConstraint.activate([
toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
toolbar.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor)
])
// Toolbar sits at the safe area bottom when keyboard is hidden,
// rises automatically when the keyboard appears — no animation code needed.// DON'T DO THIS — the guide already handles animation
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil, queue: .main
) { _ in
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded() // Unnecessary — guide does this automatically
}
}// Replaced entirely by one constraint to keyboardLayoutGuide.topAnchor
NotificationCenter.default.addObserver(
self, selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification, object: nil
)
@objc func keyboardWillShow(notification: Notification) {
let info = notification.userInfo
if let endRect = info?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
var offset = view.bounds.size.height - endRect.origin.y
if offset == 0.0 { offset = view.safeAreaInsets.bottom }
UIView.animate(withDuration: 0.25) {
self.keyboardHeight.constant = offset
self.view.layoutIfNeeded()
}
}
}
// All of this is unnecessary on iOS 15+ — use keyboardLayoutGuide instead.Version caveat: iOS 15.0–15.3 had serious bugs (FB9733654, FB9754794) where the keyboard layout guide frame was not updated correctly during view controller transitions. Reliable operation requires iOS 15.4+. For apps supporting iOS 15.0–15.3, fall back to notification-based handling.
By default, UIKeyboardLayoutGuide ignores undocked, floating, and split keyboards on iPad — when the keyboard undocks, the guide drops to the screen bottom as if the keyboard were dismissed. The followsUndockedKeyboard property (available since iOS 15.0, default false) changes this behavior.
When set to true, the guide tracks the keyboard wherever it moves. However, a single set of fixed constraints cannot handle all positions of a floating keyboard. The UITrackingLayoutGuide superclass provides edge-aware constraint activation methods for this.
view.keyboardLayoutGuide.followsUndockedKeyboard = true
// When keyboard is NOT near the top: pin toolbar above keyboard
let awayFromTop = toolbar.bottomAnchor.constraint(
equalTo: view.keyboardLayoutGuide.topAnchor
)
view.keyboardLayoutGuide.setConstraints(
[awayFromTop], activeWhenAwayFrom: .top
)
// When keyboard IS near the top: drop toolbar to safe area bottom
let nearTop = toolbar.bottomAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.bottomAnchor
)
view.keyboardLayoutGuide.setConstraints(
[nearTop], activeWhenNearEdge: .top
)
// Center toolbar horizontally when keyboard is away from edges
let centered = toolbar.centerXAnchor.constraint(
equalTo: view.keyboardLayoutGuide.centerXAnchor
)
view.keyboardLayoutGuide.setConstraints(
[centered], activeWhenAwayFrom: [.leading, .trailing]
)view.keyboardLayoutGuide.followsUndockedKeyboard = true
// These fixed constraints break when the floating keyboard is at the top
// (toolbar goes offscreen) or near an edge (misaligned)
toolbar.bottomAnchor.constraint(
equalTo: view.keyboardLayoutGuide.topAnchor
).isActive = true
toolbar.centerXAnchor.constraint(
equalTo: view.keyboardLayoutGuide.centerXAnchor
).isActive = true
// ❌ Will cause layout issues in many keyboard positions!usesBottomSafeArea and keyboardDismissPaddingTwo properties added in iOS 17.0 refine the guide's behavior:
usesBottomSafeArea (default true): when false, the guide extends to the screen's physical bottom edge when the keyboard is hidden, instead of stopping at the safe area. Useful for edge-to-edge content that should fill the home indicator region.keyboardDismissPadding (default 0): adds padding above the keyboard for UIScrollView.keyboardDismissMode = .interactive, extending the interactive dismiss gesture zone upward.// Extend guide to physical bottom (ignore safe area when keyboard hidden)
view.keyboardLayoutGuide.usesBottomSafeArea = false
// Add 60pt of interactive dismiss zone above the keyboard
view.keyboardLayoutGuide.keyboardDismissPadding = 60| Property | iOS Version | Default |
|---|---|---|
keyboardLayoutGuide | 15.0+ | N/A |
followsUndockedKeyboard | 15.0+ | false |
usesBottomSafeArea | 17.0+ | true |
keyboardDismissPadding | 17.0+ | 0 |
When you constrain a UIScrollView's bottom to view.keyboardLayoutGuide.topAnchor, the scroll view's frame shrinks as the keyboard appears. This is fundamentally different from the old notification approach of adjusting contentInset — the constraint compresses the frame via Auto Layout, the system's contentInsetAdjustmentBehavior continues handling safe area insets normally, and animations happen automatically.
override func viewDidLoad() {
super.viewDidLoad()
scrollView.translatesAutoresizingMaskIntoConstraints = false
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
)
])
scrollView.keyboardDismissMode = .interactive
// That's it. No notifications, no contentInset manipulation.
}NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: inputBar.topAnchor),
inputBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
inputBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
inputBar.bottomAnchor.constraint(
equalTo: view.keyboardLayoutGuide.topAnchor
)
])// Constraint shrinks the frame...
scrollView.bottomAnchor.constraint(
equalTo: view.keyboardLayoutGuide.topAnchor
).isActive = true
// ...AND notification handler also adjusts contentInset — DOUBLE INSET
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil, queue: .main
) { notification in
let keyboardHeight = /* ... */
self.scrollView.contentInset.bottom = keyboardHeight // ❌ Content shifts up TWICE
self.scrollView.verticalScrollIndicatorInsets.bottom = keyboardHeight
}// ❌ Using IQKeyboardManager or similar alongside keyboardLayoutGuide
// They will fight each other — disable the library when using the guide
scrollView.bottomAnchor.constraint(
equalTo: view.keyboardLayoutGuide.topAnchor
).isActive = true
// IQKeyboardManager is still active → conflicts and visual glitchesImportant limitation: the keyboard layout guide does not automatically scroll to make a focused text field visible. You still need scrollRectToVisible(_:animated:) or equivalent logic triggered by UITextFieldDelegate/UITextViewDelegate methods.
Stage Manager note (iOS 16+): Keyboard notifications can be unreliable with Stage Manager and the out-of-process keyboard architecture. The keyboard layout guide is Apple's recommended approach as it works reliably across all multitasking configurations.
For apps targeting iOS 14 and earlier, or as a fallback for iOS 15.0–15.3 bugs, notification-based handling remains necessary. Three details determine whether the implementation works correctly: coordinate conversion, safe area subtraction, and animation curve matching.
The keyboard frame from keyboardFrameEndUserInfoKey is in screen coordinate space. You must convert it to your view's coordinates — failure to do so breaks behavior in Split View, Slide Over, Stage Manager, and landscape orientation. On iOS 16.1+, the notification.object is the UIScreen, enabling screen-based conversion. On earlier versions, use view.convert(_:from: view.window).
The converted keyboard height includes the home indicator region (~34pt on Face ID devices). Since UIScrollView with default contentInsetAdjustmentBehavior (.automatic) already adds safeAreaInsets.bottom to adjustedContentInset, you must subtract view.safeAreaInsets.bottom to avoid double-insetting.
The keyboard uses an undocumented animation curve value of 7, which doesn't match any public UIView.AnimationCurve case (0–3). The UIView.AnimationOptions raw value encodes the curve in bits 16–19, so bit-shifting left by 16 converts the raw integer to the correct option.
final class KeyboardAwareViewController: UIViewController {
@IBOutlet private weak var scrollView: UIScrollView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(
self, selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification, object: nil
)
NotificationCenter.default.addObserver(
self, selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification, object: nil
)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(
self, name: UIResponder.keyboardWillShowNotification, object: nil
)
NotificationCenter.default.removeObserver(
self, name: UIResponder.keyboardWillHideNotification, object: nil
)
}
@objc private func keyboardWillShow(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let frameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double,
let curveRaw = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt
else { return }
// 1. Convert from screen coordinates to view coordinates
let keyboardViewFrame = view.convert(frameValue.cgRectValue, from: view.window)
// 2. Subtract safe area bottom to avoid double-insetting
let bottomInset = keyboardViewFrame.height - view.safeAreaInsets.bottom
// 3. Bit-shift curve value by 16 for AnimationOptions
let options = UIView.AnimationOptions(rawValue: curveRaw << 16)
UIView.animate(withDuration: duration, delay: 0, options: options) {
self.scrollView.contentInset.bottom = bottomInset
self.scrollView.verticalScrollIndicatorInsets.bottom = bottomInset
}
}
@objc private func keyboardWillHide(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double,
let curveRaw = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt
else { return }
let options = UIView.AnimationOptions(rawValue: curveRaw << 16)
UIView.animate(withDuration: duration, delay: 0, options: options) {
self.scrollView.contentInset.bottom = 0
self.scrollView.verticalScrollIndicatorInsets.bottom = 0
}
}
}final class KeyboardObservingVC: UIViewController {
private var showToken: NSObjectProtocol?
private var hideToken: NSObjectProtocol?
override func viewDidLoad() {
super.viewDidLoad()
showToken = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil, queue: .main
) { [weak self] notification in
self?.handleKeyboardShow(notification)
}
hideToken = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillHideNotification,
object: nil, queue: .main
) { [weak self] notification in
self?.handleKeyboardHide(notification)
}
}
deinit {
// Block-based observers MUST be explicitly removed
if let token = showToken { NotificationCenter.default.removeObserver(token) }
if let token = hideToken { NotificationCenter.default.removeObserver(token) }
}
}@objc func keyboardWillShow(_ notification: Notification) {
let frame = (notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
// ❌ Screen coordinates used directly — breaks in Split View, landscape, Stage Manager
scrollView.contentInset.bottom = frame.height
}let converted = view.convert(kbFrame, from: view.window)
// ❌ Full keyboard height includes ~34pt home indicator already in adjustedContentInset
scrollView.contentInset.bottom = converted.height // Double-insets on Face ID devices!// ❌ Hardcoded 0.25s and easeInOut does NOT match the keyboard's actual curve
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) {
self.scrollView.contentInset.bottom = keyboardHeight
}// ❌ Token is discarded — observer fires forever and captures self strongly
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil, queue: .main
) { notification in
self.handleKeyboard(notification) // ❌ Strong capture of self
}adjustedContentInset vs contentInset is the critical distinctionSince iOS 11, Apple split scroll view insets into two properties. contentInset (read-write) is what you set manually. adjustedContentInset (read-only) is the effective total, computed as contentInset plus the system's safe area contribution based on contentInsetAdjustmentBehavior.
| Behavior | Effect |
|---|---|
.automatic (default) | Adds safe area insets along scrollable axes; special handling inside navigation controllers |
.scrollableAxes | Adds safe area only where content is scrollable or alwaysBounces is true |
.always | Adds safe area on all edges unconditionally |
.never | No automatic adjustment — adjustedContentInset == contentInset |
adjustedContentInset for effective insets// contentInset only reflects what YOU set — not what the system added
func visibleContentHeight() -> CGFloat {
let adjusted = scrollView.adjustedContentInset
return scrollView.bounds.height - adjusted.top - adjusted.bottom
}class CustomScrollView: UIScrollView {
override func adjustedContentInsetDidChange() {
super.adjustedContentInsetDidChange()
// React to effective inset changes (safe area rotation, etc.)
invalidateLayout()
}
}.never for full-screen edge-to-edge content// Image viewers, maps, or full-bleed media where YOU handle safe areas
imageScrollView.contentInsetAdjustmentBehavior = .never
// Now adjustedContentInset == contentInset at all times
// WARNING: safe area insets propagate to subviews instead of being consumedcontentInset when you mean adjustedContentInsetfunc calculateVisibleHeight() -> CGFloat {
let inset = scrollView.contentInset // ❌ Only your manual inset, not the effective total
return scrollView.bounds.height - inset.top - inset.bottom
// On notched iPhone, this ignores the 34pt home indicator safe area
}.never without understanding side effects// ❌ Using .never as a quick fix without handling consequences
scrollView.contentInsetAdjustmentBehavior = .never
// Content now goes under navigation bars and home indicator
// Safe area insets propagate to subviews — unexpected child layout changes@objc func keyboardWillHide(_ notification: Notification) {
scrollView.contentInset = .zero // ❌ Wipes out ALL insets, including custom top/left/right
}
// ✅ Fix: only reset the bottom
@objc func keyboardWillHideCorrectly(_ notification: Notification) {
scrollView.contentInset.bottom = 0
}The interaction with keyboard handling is direct: when you set contentInset.bottom = keyboardHeight and contentInsetAdjustmentBehavior is .automatic, the system adds safeAreaInsets.bottom on top — producing the double-inset bug. Either subtract safeAreaInsets.bottom from the keyboard height, or use UIKeyboardLayoutGuide which sidesteps the issue entirely through frame compression rather than inset manipulation.
inputAccessoryView is a property on UIResponder (since iOS 3.2) that UIKit reads when a responder becomes first responder. UITextField and UITextView redeclare it as read-write, so you assign directly. UIViewController exposes it as read-only, requiring an override plus canBecomeFirstResponder returning true.
private func createToolbar() -> UIToolbar {
let toolbar = UIToolbar(
frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44)
)
toolbar.sizeToFit()
let spacer = UIBarButtonItem(
barButtonSystemItem: .flexibleSpace, target: nil, action: nil
)
let done = UIBarButtonItem(
barButtonSystemItem: .done, target: self, action: #selector(dismissKeyboard)
)
toolbar.items = [spacer, done]
return toolbar
}
override func viewDidLoad() {
super.viewDidLoad()
amountTextField.inputAccessoryView = createToolbar()
}class ChatViewController: UIViewController {
private lazy var composeBar: ComposeBarView = {
let bar = ComposeBarView()
bar.autoresizingMask = .flexibleHeight // Required for dynamic height
return bar
}()
override var inputAccessoryView: UIView? { composeBar }
override var canBecomeFirstResponder: Bool { true }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
becomeFirstResponder()
}
}class GrowingInputBar: UIView {
let textView = UITextView()
override init(frame: CGRect) {
super.init(frame: frame)
autoresizingMask = .flexibleHeight // CRITICAL for dynamic sizing
textView.translatesAutoresizingMaskIntoConstraints = false
textView.isScrollEnabled = false // Allows growth
textView.font = .systemFont(ofSize: 16)
addSubview(textView)
NSLayoutConstraint.activate([
textView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
textView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
textView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
textView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
])
}
required init?(coder: NSCoder) { fatalError() }
// CRITICAL: must override for dynamic sizing
override var intrinsicContentSize: CGSize {
let textSize = textView.sizeThatFits(
CGSize(width: textView.bounds.width, height: .greatestFiniteMagnitude)
)
let height = min(textSize.height + 16, 120)
return CGSize(width: UIView.noIntrinsicMetric, height: height)
}
}
// Trigger resize when text changes:
func textViewDidChange(_ textView: UITextView) {
inputBar.invalidateIntrinsicContentSize()
}let toolbar = UIToolbar() // ❌ Frame is .zero — toolbar will be invisible
toolbar.items = [doneButton]
textField.inputAccessoryView = toolbaroverride var inputAccessoryView: UIView? {
return UIToolbar(frame: ...) // ❌ Creates a NEW toolbar on every property access
}
// ✅ Fix: use a lazy var
private lazy var toolbar: UIToolbar = { /* configure once */ }()
override var inputAccessoryView: UIView? { toolbar }class BrokenVC: UIViewController {
override var inputAccessoryView: UIView? { someToolbar }
// ❌ Missing canBecomeFirstResponder — accessory will never appear
}class LeakyVC: UIViewController {
private lazy var bar: UIView = { UIView(frame: .init(x: 0, y: 0, width: 320, height: 44)) }()
override var inputAccessoryView: UIView? { bar }
override var canBecomeFirstResponder: Bool { true }
// ❌ VC retains bar, system retains VC via responder chain → deinit NEVER called
}
// ✅ Workaround: nil out in viewDidDisappear and resign first responder
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
resignFirstResponder()
}inputAccessoryViewController (iOS 8+) provides a view-controller-based alternative. It takes precedence over inputAccessoryView if both are set, provides full lifecycle methods (viewDidLoad, etc.), and its UIInputView is automatically styled to match the keyboard. Use it for complex accessory bars that benefit from containment, or as a workaround for the inputAccessoryView retain cycle.
Design choice: use inputAccessoryView for persistent bars that stay visible when the keyboard hides (Messages-style), and keyboardLayoutGuide for adjusting existing layout elements. Avoid combining both on the same view controller.
WWDC 2025 (June 9, 2025) introduced iOS 26 with one major keyboard API improvement: strongly typed notification messages that eliminate the fragile userInfo dictionary casting pattern.
NotificationCenter.Message APIUIKit in iOS 26 represents each keyboard notification as a dedicated NotificationCenter.Message type. Properties like animationDuration and endFrame are available directly on the message object.
// iOS 26+: Type-safe keyboard notification handling
NotificationCenter.default.addObserver(
forMessage: UIResponder.keyboardWillShowMessage
) { message in
let duration = message.animationDuration // Double, no casting
let frame = message.endFrame // CGRect, no casting
UIView.animate(withDuration: duration) {
self.bottomConstraint.constant = frame.height
self.view.layoutIfNeeded()
}
}// Pre-iOS 26: fragile dictionary lookups with manual casting
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil, queue: .main
) { notification in
guard let userInfo = notification.userInfo,
let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
else { return } // ❌ Verbose, error-prone, fails silently on type mismatch
}UIKeyboardLayoutGuide received no new APIs in iOS 26. The properties introduced through iOS 17 (usesBottomSafeArea, keyboardDismissPadding, followsUndockedKeyboard) remain the latest additions. The inputAccessoryView API is also unchanged, including the long-standing memory leak when used on UIViewController.
Two other iOS 26 features are tangentially relevant to keyboard handling. updateProperties() is a new UIView/UIViewController method that runs before layoutSubviews() and automatically tracks Observable objects — useful for keyboard-responsive layout updates. The flushUpdates animation option simplifies animating constraint changes, which can clean up keyboard animation code in notification handlers.
The Liquid Glass visual redesign in iOS 26 gives the keyboard translucent, glass-like key caps. This is automatic for apps compiled against the iOS 26 SDK and requires no code changes, but custom inputAccessoryView styling may need visual updates to match the new aesthetic.
The decision tree for keyboard handling in 2024–2026 is straightforward. For iOS 15.4+ targets, use UIKeyboardLayoutGuide — pin your content or scroll view bottom to view.keyboardLayoutGuide.topAnchor and let Auto Layout handle animation, safe area tracking, and keyboard height changes. For iPad floating keyboards, enable followsUndockedKeyboard with edge-aware adaptive constraints. For scroll views, constrain the frame to the guide and avoid touching contentInset for keyboard purposes.
When supporting older versions, the notification-based approach requires three non-negotiable steps: convert the keyboard frame from screen to view coordinates, subtract view.safeAreaInsets.bottom from the height, and bit-shift the animation curve by 16 to create UIView.AnimationOptions. Always read adjustedContentInset (not contentInset) when calculating visible content area. For input bars, set autoresizingMask = .flexibleHeight, override intrinsicContentSize, and clean up in viewDidDisappear to avoid the inputAccessoryView retain cycle.
NotificationCenter.Message APIs eliminate the most error-prone part of the legacy approach — though if you're targeting iOS 26, you should already be using UIKeyboardLayoutGuide instead of notifications entirely.UIKeyboardLayoutGuide (iOS 15+) used — not manual keyboard notification handlingview.keyboardLayoutGuide.topAnchorfollowsUndockedKeyboard = true set for floating/split keyboard trackingview.convert(_:from: view.window)view.safeAreaInsets.bottom to avoid double-insettingkeyboardAnimationCurveUserInfoKeycontentInsetAdjustmentBehavior set appropriately on scroll views