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.
If you enjoyed this article, please feel free to follow me on my social media: