Im Zuge eines Projekts stießen meine Kollegen und ich auf das folgende Problem: Ist ein TextEditor
Teil einer Form
-View, scrollt jene Form
-View nicht automatisch zum TextEditor
, wenn dieser den Fokus erhält. Das führt dazu, dass die virtuelle Bildschirmtastatur sich unter iOS über den aktiven TextEditor
legt, wenn dieser sich als Teil einer Form
-View am unteren Bildschirmrand befindet.
Ein Beispiel, mit dem ihr diese Problematik nachvollziehen könnt, zeigt das folgende Listing. Wechselt ihr darin in die Zelle des Text-Editors, überlagert die erscheinende Bildschirmtastatur den Eingabebereich. Man muss manuell nach unten scrollen, um den Text-Editor sehen zu können.
struct ContentView: View {
@State private var text = ""
var body: some View {
Form {
ForEach(0 ..< 20) { value in
Text("Dummy text \(value)")
}
TextEditor(text: $text)
}
}
}
Um das Problem zu lösen, griff ich auf zwei Mechanismen zurück:
FocusState
undScrollViewReader
FocusState
ermöglicht es mir zu erfahren, wenn eine View wie TextEditor
den Fokus erhält. Eine ergänzende Implementierung von FocusState
auf Basis des vorangegangenen Listings zeigt der folgende Code:
struct ContentView: View {
@State private var text = ""
@FocusState private var textEditorHasFocus
var body: some View {
Form {
ForEach(0 ..< 20) { value in
Text("Dummy text \(value)")
}
TextEditor(text: $text)
.focused($textEditorHasFocus)
}
}
}
So gibt der neue textEditorHasFocus
-Status Aufschluss darüber, ob der Text-Editor gerade aktiv ist oder nicht.
Das zweite wichtige Element zur Lösung des Problems ist der ScrollViewReader
. Es handelt sich hierbei um eine View, die als Parameter eine scrollbare View erhält. Zusätzlich gibt ScrollViewReader
uns Zugriff auf eine Instanz vom Typ ScrollViewProxy
. Dieser Proxy greift für die gesamte View, die ScrollViewReader
umschließt.
Doch wozu das ganze? ScrollViewProxy
ermöglicht es uns, zu einer bestimmten Stelle innerhalb einer Scroll-View zu scrollen! So können wir, sobald textEditorHasFocus
dem Wert true
entspricht, zum Text-Editor wechseln.
Damit das funktioniert, muss ScrollViewProxy
die View, zu der wir scrollen möchten, aber identifizieren können. Am einfachsten definiert man dazu einen Namespace mithilfe des gleichnamigen Property Wrappers. Diesen Namespace weist man dann der gewünschten View über den id(_:)
-Modifier zu.
Durch Aufruf der Methode scrollTo(_:anchor:)
der ScrollViewProxy
-Instanz kann man dann zum gewünschten Namespace springen. Jenen Aufruf implementiere ich als Teil des onChange(of:perform:)
-Modifiers, über den ich prüfe, ob sich textEditorHasFocus
geändert hat. Ist das der Fall und entspricht der Status true
, scrolle ich zum Text-Editor. Die vollständige Implementierung zeigt das folgende Listing:
struct ContentView: View {
@Namespace var textEditorID
@State private var text = ""
@FocusState private var textEditorHasFocus
var body: some View {
ScrollViewReader { scrollViewProxy in
Form {
ForEach(0 ..< 20) { value in
Text("Dummy text \(value)")
}
TextEditor(text: $text)
.focused($textEditorHasFocus)
.id(textEditorID)
}
.onChange(of: textEditorHasFocus) { newTextEditorHasFocus in
if newTextEditorHasFocus {
Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in
scrollViewProxy.scrollTo(textEditorID)
}
}
}
}
}
}
Wie ihr seht, muss ich mich noch aber eines weiteren kleinen Kniffs bedienen: Sobald textEditorHasFocus
dem Wert true
entspricht, muss ich erst kurz warten, ehe ich die Scroll-Aktion durch Aufruf von scrollTo(_:anchor:)
starte. Ansonsten findet das Scrolling nämlich statt, ehe die Bildschirmtastatur die sichtbare View verkleinert hat. Wir würden also zum Text-Editor scrollen, der sich zu diesem Zeitpunkt noch immer am unteren Bildschirmrand befindet. Erst dann würde die Bildschirmtastatur auftauchen, die sich erneut über den sichtbaren Bereich des Text-Editors legt.
Mithilfe eines Timers steuere ich aus diesem Grund, dass das Scrolling erst nach einer Verzögerung von 0,3 Sekunden stattfindet. Die virtuelle Bildschirmtastatur hat die zugrundeliegende View dann bereits verkleinert und wir gelangen erfolgreich zum Text-Editor.
Der letzte Schwung
Eine finale Optimierung, die ihr vornehmen könnt, ist das Scrollen mit einer Animation zu verknüpfen. Dazu packt ihr den scrollTo(_:anchor:)
-Aufruf in einen withAnimation(_:_:)
-Block, so wie im folgenden Listing zu sehen.
struct ContentView: View {
@Namespace var textEditorID
@State private var text = ""
@FocusState private var textEditorHasFocus
var body: some View {
ScrollViewReader { scrollViewProxy in
Form {
ForEach(0 ..< 20) { value in
Text("Dummy text \(value)")
}
TextEditor(text: $text)
.focused($textEditorHasFocus)
.id(textEditorID)
}
.onChange(of: textEditorHasFocus) { newTextEditorHasFocus in
if newTextEditorHasFocus {
Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in
withAnimation {
scrollViewProxy.scrollTo(textEditorID)
}
}
}
}
}
}
}
Durch diese Änderung wird der Text-Editor in einer schönen Animation angesteuert, statt plötzlich direkt sichtbar zu sein.
Euer Thomas
Schreibe einen Kommentar