Keyboard-Responder in SwiftUI

Eine der häufigsten Fragen, die mir immer wieder im Zusammenhang mit SwiftUI gestellt wird, bezieht sich auf das Zusammenspiel mit der virtuellen Bildschirmtastatur unter iOS und iPadOS. Wie reagiert man auf das Erscheinen und Verschwinden dieser Bildschirmtastatur in SwiftUI? Und wie passt man die Größe von SwiftUI-Views dann korrekt an?

Da das Thema mit dem Erscheinen der TextEditor-View in der neuesten SwiftUI-Version weiter an Relevanz gewinnen wird, möchte ich euch sowohl in Videos auf meinem YouTube-Kanal als auch hier auf dem Blog einmal meine zugehörige Lösung präsentieren.

Basis: KeyboardResponder-Klasse

Generell fängt man Informationen zum Ein- und Ausblenden der Bildschirmtastatur auch bei der Arbeit mit SwiftUI mit den entsprechenden System-Notifications aus UIKit ab. Das Erscheinen der Bildschirmtastatur wird von der Notification UIResponder.keyboardWillShowNotification begleitet, das Ausblenden von UIResponder.keyboardWillHideNotification.

Da in SwiftUI alle View-Updates auf einem Status und entsprechenden Statusveränderungen basieren, nutzen wir diese Notifications im Zusammenspiel mit einem eigenen Typ. Ich nenne ihn gerne KeyboardResponder. Er ist konform zum ObservableObject-Protokoll, was es erlaubt, Instanzen dieser Klasse als Status in SwiftUI-Views zu verwenden.

Die wichtigste Eigenschaft, um eine View nach Ein- und Ausblenden der Bildschirmtastatur zu aktualisieren, ist die Höhe der Bildschirmtastatur. Sie ist maßgeblich dafür verantwortlich, wie viel Platz uns für eine SwiftUI-View auf dem Display noch zur Verfügung steht. Aus diesem Grund speichere ich jene Information als Published-Property namens keyboardHeight innerhalb von KeyboardResponder.

Bei Initialisierung von KeyboardResponder registriert sich jene Instanz für die oben genannten Notifications. Werden sie ausgelöst, folgt eine passende Aktualisierung der Published-Property keyboardHeight. Bei Erscheinen der Bildschirmtastatur liest man dynamisch deren Höhe aus und weist sie keyboardHeight zu. Beim Ausblenden setzt man keyboardHeight auf den Standardwert 0 zurück. Ein Wert von 0 bedeutet in diesem Kontext, dass die Bildschirmtastatur nicht sichtbar ist. Das gilt beispielsweise auch dann, wenn der Nutzer sein iOS-Gerät mit einer externen Tastatur gekoppelt hat.

Zusätzlich bringt KeyboardResponder noch eine Hilfsmethode namens bottomInset(includingHeight:) mit. Sollte die Bildschirmtastatur sichtbar sein, zieht die Methode von deren Höhe noch einen übergebenen Wert ab. Das kann man nutzen, um eine spezifische Höhe bei Erscheinen der Bildschirmtastatur auszuschließen.

Ein gutes Beispiel hierfür ist eine Tab-Bar. Die soll in der Regel von der Bildschirmtastatur überlagert werden. Im Umkehrschluss bedeutet das aber, dass man die Höhe der Tab-Bar von der Höhe der Bildschirmtastatur abziehen muss. Andernfalls schränkt man den sichtbaren Bereich unnötig ein.

Die vollständige Implementierung meiner KeyboardResponder-Klasse findet ihr im folgenden Listing.

final class KeyboardResponder: ObservableObject {
    
    @Published var keyboardHeight: CGFloat = 0
    
    init() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow(notification:)),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide(notification:)),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
    }
    
    func bottomInset(includingHeight includedHeight: CGFloat) -> CGFloat {
        if keyboardHeight == 0 {
            return keyboardHeight
        }
        return keyboardHeight - includedHeight
    }
    
    @objc private func keyboardWillShow(notification: Notification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
            keyboardHeight = keyboardSize.height
        }
    }
    
    @objc private func keyboardWillHide(notification: Notification) {
        keyboardHeight = 0
    }
    
}

Einsatz in der Praxis

Um die Funktionsweise von KeyboardResponder auf die Probe zu stellen, findet ihr im Folgenden ein passendes Praxisbeispiel. Die eigentliche Anwendung von KeyboardResponder ist hierbei im Kern recht simpel. Zum einen müsst ihr eine Instanz der Klasse als ObservedObject innerhalb der gewünschten View registrieren. Der zweite Schritt besteht darin festzulegen, wie genau sich das Ein- und Ausblenden der Bildschirmtastatur auswirkt. Meist sollen Views vom unteren Bildschirmrand her verkleinert werden. Dazu kann man den padding(_:_:)-Modifier nutzen. Als ersten Parameter übergibt man .bottom, um festzulegen, dass die Einrückung einer View von unten her erfolgen soll. Im Anschluss übergibt man die gewünschte Höhe für die Einrückung. Im einfachsten Fall ist das die Höhe der Bildschirmtastatur, sprich der Wert von keyboardHeight unserer KeyboardResponder-Instanz.

Wann immer sich nun keyboardHeight ändert, erfolgt automatisch eine passende Aktualisierung der View.

Das gezeigte Beispiel ist aber insbesondere deshalb so interessant, weil es auf einer TabView basiert. Wie bereits angemerkt, können wir die Höhe der Tab-Bar einer TabView von der Höhe der Bildschirmtastatur abziehen, wenn wir eine Einrückung für eine View innerhalb einer TabView vornehmen (genau so wie in meinem Beispiel).

Um die Höhe einer Tab-Bar in SwiftUI zu ermitteln, muss man auf die GeometryReader-View zurückgreifen. Man benötigt sie, um einerseits die Höhe der TabView als auch die Höhe der View innerhalb von TabView zu ermitteln (in diesem Fall enthält TabView eine NavigationView). Die Differenz aus der Höhe der TabView und der in ihr enthaltenen View ergibt die Höhe der Tab-Bar.

Ebenfalls wichtig: Man muss auch die Safe Area beachten, die zusätzlich am oberen und unteren Rand noch Platz beansprucht. Die Höhe der Safe Area ist auf die Höhe der TabView zu addieren, bevor man die Höhe der in der TabView enthaltenen View abzieht. Das führt auch zu der auf den ersten Blick etwas komplex anmutenden Rechnung in meinem Beispiel, die zur Kalkulation der passenden Einrückung innerhalb des padding(_:_:)-Aufrufs durchgeführt wird.

struct ContentView: View {
    @State private var someValue = ""
    
    @ObservedObject private var keyboardResponder = KeyboardResponder()
    
    var body: some View {
        GeometryReader { tabViewGeometryProxy in
            TabView {
                GeometryReader { navigationViewGeometryProxy in
                    NavigationView {
                        Form {
                            ForEach(0 ..< 30) { value in
                                TextField("TextField \(value)", text: self.$someValue)
                            }
                        }
                        .navigationBarTitle("Keyboard Test")
                        .padding(.bottom, self.keyboardResponder.bottomInset(includingHeight: tabViewGeometryProxy.size.height + tabViewGeometryProxy.safeAreaInsets.bottom - navigationViewGeometryProxy.size.height))
                    }
                }
                .tabItem {
                    Image(systemName: "book.fill")
                    Text("Keyboard Test")
                }
            }
        }
    }
}

Fazit

Mithilfe einer Klasse wie KeyboardResponder kann man auch in SwiftUI-Views sehr komfortabel auf das Ein- und Ausblenden der Bildschirmtastatur reagieren. Dazu setzt man Instanzen dieser Klasse überall dort als Status ein, wo der Umgang mit der Bildschirmtastatur wichtig ist. Dann muss man sich nur noch darum kümmern, die korrekte Höhe für die Einrückung der View zu kalkulieren und mittels Modifier zu setzen. Wie die zugehörige Berechnung aussieht, hängt von Fall zu Fall und dem Aufbau sowie der Struktur der zugrundeliegenden View ab.

Euer Thomas

Weiterführende Links zum Artikel


Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert