How to add padding between a TextField and the keyboard in SwiftUI


Greetings, traveler!

Since the release of iOS 14, SwiftUI has included a built-in keyboard avoidance feature. For example, when you have a TextField inside your view, this view automatically moves up to keep the input area visible when the TextField becomes active. This is very convenient! However, what if we want to add some extra space below the TextField?

We can use Combine and NotificationCenter to do such things. But first, let’s create a View.

struct ContentView: View {
    
    @State var text: String = ""
    
    var body: some View {
        Spacer()
        TextField("", text: $text)
            .background {
                Color.gray
            }
    }
}

Now, we can create a ViewModifier to handle keyboard avoidance. We can pass an inset value with its initializer.

struct KeyboardAvoiding: ViewModifier {
    
    let inset: CGFloat
    
}

Then, we will create a computed property with the AnyPublisher<Bool, Never> type. Here, we will use NotificationCenter to retrieve keyboard-associated notifications.

struct KeyboardAvoiding: ViewModifier {
    
    let inset: CGFloat
    
    private var keyboardPublisher: AnyPublisher<Bool, Never> {
        Publishers.Merge(
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillShowNotification)
                .map { _ in true },
            
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillHideNotification)
                .map { _ in false }
        )
        .eraseToAnyPublisher()
    }
    
}

Now, let’s move to the body function. Here, we will use a safeAreaInset modifier that shows the specified content above or below the modified view. We will select the bottom edge option to display our content below the TextField.

struct KeyboardAvoiding: ViewModifier {
    
    let inset: CGFloat
    
    private var keyboardPublisher: AnyPublisher<Bool, Never> {
        Publishers.Merge(
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillShowNotification)
                .map { _ in true },
            
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillHideNotification)
                .map { _ in false }
        )
        .eraseToAnyPublisher()
    }
    
    func body(content: Content) -> some View {
        content
            .safeAreaInset(edge: .bottom) {
                
            }
    }
    
}

We can use a EmptyView to create spacing. Then, we will add a frame modifier to specify its height. Since we want to show an inset only when the keyboard appears, we will make a State property to pass a dynamic value.

struct KeyboardAvoiding: ViewModifier {
    
    let inset: CGFloat
    
    @State private var keyboardPadding: CGFloat = .zero
    private var keyboardPublisher: AnyPublisher<Bool, Never> {
        Publishers.Merge(
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillShowNotification)
                .map { _ in true },
            
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillHideNotification)
                .map { _ in false }
        )
        .eraseToAnyPublisher()
    }
    
    func body(content: Content) -> some View {
        content
            .safeAreaInset(edge: .bottom) {
                EmptyView()
                    .frame(height: keyboardPadding)
            }
    }
    
}

To perform changes, we will use an onRecieve modifier, which will help us handle any action when our view detects data emitted by our publisher.

struct KeyboardAvoiding: ViewModifier {
    
    let inset: CGFloat
    
    @State private var keyboardPadding: CGFloat = .zero
    private var keyboardPublisher: AnyPublisher<Bool, Never> {
        Publishers.Merge(
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillShowNotification)
                .map { _ in true },
            
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillHideNotification)
                .map { _ in false }
        )
        .eraseToAnyPublisher()
    }
    
    func body(content: Content) -> some View {
        content
            .safeAreaInset(edge: .bottom) {
                EmptyView()
                    .frame(height: keyboardPadding)
            }
            .onReceive(keyboardPublisher) { isPresented in
                keyboardPadding = isPresented ? inset : .zero
            }
    }
    
}

As the last step, we can create an extension to make our code cleaner.

extension View {
    func keyboardAvoiding(_ inset: CGFloat) -> some View {
        modifier(KeyboardAvoiding(inset: inset))
    }
}

And that’s it! Now, we can manage the inset below any input view.

struct ContentView: View {
    
    @State var text: String = ""
    
    var body: some View {
        Spacer()
        TextField("", text: $text)
            .background {
                Color.gray
            }
            .keyboardAvoiding(25)
    }
}

Conclusion

There are no built-in solutions in SwiftUI to manage keyboard avoidance so far, but it offers convenient tools to create one without serious effort.