Skip to content

Instantly share code, notes, and snippets.

@kibotu
Last active October 30, 2025 09:26
Show Gist options
  • Select an option

  • Save kibotu/35c3fa779ca1859bd48bcfc4fb9a9415 to your computer and use it in GitHub Desktop.

Select an option

Save kibotu/35c3fa779ca1859bd48bcfc4fb9a9415 to your computer and use it in GitHub Desktop.
SwiftUI & UIKit Keyboard Avoidance fix for iOS 26.0.1, 26.1

Keyboard Avoidance Implementation

Overview

This document describes the keyboard avoidance implementation for the Messenger iOS app, ensuring that the ChatInputView is not overlapped by the keyboard on iOS 16+ (including iOS 16.0.1 and 16.1).

Problem

The SwiftUI-based ChatInputView was not properly avoiding the keyboard when it appeared, potentially causing the input field to be hidden behind the keyboard.

Solution

1. KeyboardAdaptiveModifier

A custom ViewModifier that listens to keyboard notifications and adds appropriate bottom padding.

Location: Sources/C24ProfisNativeMessenger/Shared/Modifier/KeyboardAdaptiveModifier.swift

How it works:

  • Subscribes to Publishers.keyboardHeight (already available in the codebase)
  • Calculates keyboard height minus safe area insets to avoid double-counting
  • Adds dynamic bottom padding with smooth animation
  • Uses GeometryReader in background to avoid layout interference
.keyboardAdaptive()

2. Applied to MainView

The modifier is applied to the main VStack in MainView.swift at line 86:

VStack(spacing: 0) {
    // ... message list and attachments ...
    ChatInputView(viewModel: viewModel)
}
.keyboardAdaptive()  // ← Applied here
.background(viewModel.config.lookAndFeel.backgroundColor.color)

3. ChatInputView Background Handling

The ChatInputView maintains .ignoresSafeArea(edges: .bottom) on its background color (line 33) to ensure the background extends all the way to the bottom of the screen, while the content itself respects the keyboard padding from the parent.

Compatibility

iOS 16.0+

  • ✅ Uses keyboard notifications with proper safe area calculations
  • ✅ Smooth animations (0.25s ease-out)
  • ✅ Automatic keyboard dismissal on scroll (via dismissKeyboardOnScrollCompat())

iOS 15

  • ✅ Full backward compatibility
  • ✅ Uses same keyboard notification system

Testing Checklist

Test on the following iOS versions:

  • iOS 15.0
  • iOS 16.0
  • iOS 16.0.1
  • iOS 16.1
  • iOS 17.0+
  • iOS 18.0+

Test scenarios:

  • Tap on text input field - keyboard appears, input visible
  • Type long message - input expands, remains visible
  • Rotate device with keyboard open
  • Switch between apps with keyboard open
  • Test on devices with and without home button (different safe areas)
  • Test in landscape orientation
  • Scroll message list - keyboard dismisses interactively

Related Files

  1. KeyboardAdaptiveModifier.swift - Main keyboard avoidance logic
  2. MainView.swift - Application point (line 86)
  3. ChatInputView.swift - Input view with background handling (line 33)
  4. Publisher+Extensions.swift - Publishers.keyboardHeight publisher (lines 54-66)
  5. ScrollDownDismissKeyboardModifier.swift - Keyboard dismissal on scroll
  6. Keyboard.swift - Utility for programmatic keyboard dismissal

Alternative Approaches Considered

1. Native SwiftUI Keyboard Avoidance (iOS 14+)

SwiftUI has built-in keyboard avoidance, but it can be inconsistent, especially with:

  • Complex view hierarchies
  • Custom safe area modifications
  • Background views that ignore safe areas

2. UIKit Integration

The legacy MessengerViewController.swift uses UIKit-based keyboard handling:

stackView.directionalLayoutMargins.bottom = keyboardViewEndFrame.height - view.safeAreaInsets.bottom

The SwiftUI approach mirrors this logic but in a declarative way.

Troubleshooting

Issue: Input still hidden by keyboard

Possible causes:

  1. Safe area calculation incorrect - check device safe area insets
  2. Multiple .ignoresSafeArea() modifiers conflicting
  3. View hierarchy changed

Solution: Verify .keyboardAdaptive() is on the correct parent view

Issue: Double padding or gap at bottom

Possible causes:

  1. Safe area being counted twice
  2. Custom padding added elsewhere

Solution: Check geometry.safeAreaInsets.bottom calculation in modifier

Issue: Keyboard animation not smooth

Possible causes:

  1. Other animations interfering
  2. Animation duration mismatch

Solution: Adjust animation parameters in KeyboardAdaptiveModifier (line 15)

Performance Considerations

  • Keyboard notifications are infrequent (only on keyboard show/hide)
  • GeometryReader in background has minimal performance impact
  • Animation is hardware-accelerated by SwiftUI

Future Improvements

  1. Consider iOS 17+ keyboard layout guide APIs
  2. Add support for external keyboard detection
  3. Optimize for iPad split-screen scenarios
  4. Add haptic feedback on keyboard appearance (if desired)
import SwiftUI
import Combine
/// A view modifier that adds bottom padding to avoid the keyboard
struct KeyboardAdaptiveModifier: ViewModifier {
@State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
content
.padding(.bottom, keyboardHeight)
.background(
GeometryReader { geometry in
Color.clear
.onReceive(Publishers.keyboardHeight) { height in
withAnimation(.easeOut(duration: 0.25)) {
// Subtract safe area bottom inset to avoid double-counting
// The keyboard height from the notification includes the screen bottom,
// but we need to subtract the safe area since our view already respects it
let bottomInset = geometry.safeAreaInsets.bottom
keyboardHeight = max(0, height - bottomInset)
}
}
}
)
}
}
extension View {
/// Adds bottom padding when the keyboard appears to prevent content from being hidden
/// - Returns: A view that adapts to keyboard appearance
func keyboardAdaptive() -> some View {
modifier(KeyboardAdaptiveModifier())
}
}
// Environment key for safe area insets
private struct SafeAreaInsetsKey: EnvironmentKey {
static var defaultValue: EdgeInsets {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }?
.safeAreaInsets
.swiftUiInsets ?? EdgeInsets()
}
}
extension EnvironmentValues {
var safeAreaInsets: EdgeInsets {
self[SafeAreaInsetsKey.self]
}
}
private extension UIEdgeInsets {
var swiftUiInsets: EdgeInsets {
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment