Press "Enter" to skip to content

Zu TextEditor in Form-View scrollen

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 und
  • ScrollViewReader

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

Deine E-Mail-Adresse wird nicht veröffentlicht.

Impressum

Thomas Sillmann
Kettererstraße 6
D-63739 Aschaffenburg
E-Mail: contact@thomassillmann.de
Mobil: +49 (0) 151 65125650
Web: https://www.thomassillmann.de/

Inhaltlich Verantwortlicher gemäß §55 Abs. 2 RStV: Thomas Sillmann (Anschrift siehe oben)

Haftungshinweis: Trotz sorgfältiger inhaltlicher Kontrolle übernehme ich keine Haftung für die Inhalte externer Links. Für die Inhalte der verlinkten Seiten sind ausschließlich deren Betreiber verantwortlich.

Kontakt und soziale Netzwerke

© 2019-2021 by Thomas Sillmann