SwiftUI in der Praxis, Teil 1
Seite 3: Integration von UIKit Views
Eine wichtige Eigenschaft fehlt nun aber noch: die Text-View zur Anzeige und Bearbeitung des Inhalts einer Notiz (in Listing 6 an dem TODO-Kommentar zu erkennen). Das Problem ist hier: Eine View wie UITextView
aus dem UIKit-Framework gibt es in SwiftUI (noch) nicht. Um also an der Stelle voranzukommen, müssen Entwickler eine UITextView
-Instanz in die SwiftUI View einbinden. Deshalb ist für UITextView
ein sogenanntes Representable in SwiftUI anzufertigen. Dazu erstellt man zunächst innerhalb des Projekts eine neue SwiftUI View und nennt sie TextView
. Die body
-Property inklusive Inhalt können Entwickler vollständig aus der neu erzeugten Datei entfernen; die benötigen sie in diesem Fall nicht. Stattdessen legen sie zunächst fest, dass TextView
zum UIViewRepresentable
-Protokoll konform ist – statt zum View
-Protokoll.
UIViewRepresentable
erlaubt es, beliebige Klassen auf Basis von UIView
in SwiftUI einzubetten. Analog dazu stehen weitere Protokolle zur Verfügung:
UIViewControllerRepresentable
für den Einsatz vonUIViewController
-Instanzen in SwiftUI.NSViewControllerRepresentable
für die Verwendung vonNSViewController
-Instanzen in SwiftUI.NSViewRepresentable
für den Einsatz vonNSView
-Instanzen in SwiftUI.WKInterfaceObjectRepresentable
für den Einsatz vonWKInterfaceObject
-Instanzen in SwiftUI.
Der Grundaufbau aller genannten Protokolle ist ähnlich. Sie besitzen jeweils eine eigene Make- und Update-Methode, die beide zu implementieren sind. Die Make-Methode erzeugt die eigesetzten Views oder View-Controllers aus dem AppKit-, UIKit- oder WatchKit-Framework. Die Update-Methode wird immer aufgerufen, wenn sich Daten der SwiftUI-View verändern.
Bevor Entwickler mit der Implementierung des UIViewRepresentable
-Protokolls beginnen, müssen sie der neuen TextView
-Structure zunächst noch eine Binding-Property namens text
hinzufügen. Die verweist auf den Text, der innerhalb der Text-View angezeigt werden soll (also beispielsweise der Inhalt einer Notiz) und erlaubt es dank Binding, den Text auch zu verändern.
Nun geht es an die eigentliche Integration der UITextView
in SwiftUI. Dazu implementieren Entwickler zunächst die makeUIView(context:)
-Methode, deren Rückgabetyp sie explizit auf UITextView
setzen. So geben sie explizit an, dass TextView
auf UITextView
basiert. Innerhalb der Make-Methode braucht man nichts weiter zu tun, als eine passende UITextView
-Instanz zu erzeugen und zurückzugeben.
Im Anschluss folgt noch die Implementierung der Update-Methode namens updateUIView(_:context:)
. Die Methode ruft das System immer dann auf, wenn sich Daten der SwiftUI-View (sprich von TextView
) ändern. TextView
besitzt nur eine Eigenschaft namens text
. Da deren Inhalt in der UITextView
angezeigt werden soll, legen Entwickler in der Update-Methode fest, dass bei jeder Änderung von text
ihr Wert der UITextView
zugewiesen wird. Auf diese View lässt sich hierbei über den ersten Parameter der Update-Methode uiView
zugreifen.
Beim Einsatz eines Representable-Protokolls wie UIViewRepresentable
kommt aber noch eine weitere Besonderheit zum Tragen: der sogenannte Coordinator. Er ist eine unabhängige Klasse, die sich um die Kommunikation zwischen View beziehungsweise View-Controller aus AppKit, UIKit und WatchKit und der SwiftUI-View kümmert. Das ist immer dann wichtig, wenn eine View oder ein View-Controller Aktionen auslösen, die Entwickler in der zugehörigen SwiftUI-View auswerten müssen.
Ein solcher Fall liegt tatsächlich bei der Integration der UITextView
vor. Zwar können Entwickler eine solche nun in SwiftUI mit der TextView
-Structure initialisieren und ihr einen Text übergeben, jedoch fehlt noch die Logik, um Änderungen am Text aus der UITextView
heraus zu übernehmen. Dafür würde sich im einfachsten Fall die Implementierung der textViewDidChange(_:)
-Methode aus dem UITextViewDelegate
-Protokoll eignen. Entsprechend braucht es einen Coordinator, der genau diese Anforderungen erfüllt und so Änderungen am Text an die text
-Property der TextView
-Structure übergibt.
Einen solchen Coordinator erstellt man typischerweise als Nested Type der SwiftUI View. Damit sie mit jener SwiftUI View kommunizieren kann, übergibt man ihr bei der Initialisierung eine passende Referenz. Die restliche Implementierung des Coordinators kümmert sich "nur" noch darum, eine reibungslose Kommunikation zwischen SwiftUI und AppKit, UIKit oder WatchKit sicherzustellen.
Wichtig: Um einen Coordinator in einem Representable wie TextView
nutzen zu können, ist noch eine passende Make-Coordinator-Methode zu implementieren, die eine Instanz des gewünschten Coordinators erzeugt. Im Fall des UIViewRepresentable
-Protokolls lautet diese Methode makeCoordinator(_:)
, als Rückgabetyp wird der zum Coordinator passende angegeben.
Damit der Coordinator im Fall der TextView
nun seine Arbeit korrekt verrichten kann, müssen Entwickler innerhalb der makeUIView(context_:)
-Methode sicherstellen, dass er als Delegate für die erzeugte UITextView
-Instanz verwendet wird. Hierzu lässt sich der Coordinator über eine gleichnamige Property des context
-Parameters auslesen. Die vollständige Implementierung der TextView
-Structure findet man in Listing 7.
Da TextView
eine Binding-Property bei der Initialisierung erwartet, ist im zugehörigen Preview-Provider ein passender Wert zu übergeben. Da dort aber typischerweise kein Binding zur Verfügung steht, kann man auch die Funktion constant(_:)
nutzen, um einen statischen Wert für Preview-Zwecke zu übergeben.
// Listing 7: Integration einer UITextView in SwiftUI (inklusive Anpassung des Preview-Providers)
struct TextView: UIViewRepresentable {
@Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<TextView>) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<TextView>) {
uiView.text = text
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: TextView
init(_ textView: TextView) {
parent = textView
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}
struct TextView_Previews: PreviewProvider {
static var previews: some View {
TextView(text: .constant("Lorem ipsum ..."))
}
}
Nun hat man eine UITextView
als SwiftUI-View umgesetzt; inklusive passender Binding-Property, über die Entwickler den Inhalt der UITextView
steuern und Änderungen am Text übernehmen können. Nun brauchen sie nur noch eine Instanz der TextView
-Structure innerhalb der NoteView
zu erstellen. Hierfür ersetzen sie den zuvor vorbereiteten TODO-Kommentar durch einen entsprechenden Befehl und übergeben als Binding den Inhalt der Notiz. Zusätzlich nutzen sie den border(_:width:)
-Modifier, um einen Rahmen um die Text-View zu setzen und sie so vom Rest der Ansicht ein wenig abzuheben. Durch Aufruf des padding()
-Modifiers auf dem VStack
rücken Entwickler abschließend alle View-Elemente noch ein wenig ein, und auch den Titel einer Notiz zeigen sie noch einmal als Titel der Navigation-Bar an. Das kommt zum Tragen, wenn sie Notizen später wieder einblenden. Die vollständige Implementierung von NoteView
findet sich in Listing 8 (siehe auch Abb. 3):
// Listing 8: Einsatz einer UITextView in SwiftUI
struct NoteView: View {
@ObservedObject var note: Note
var body: some View {
VStack {
TextField("Title", text: $note.title)
TextView(text: $note.content)
.border(Color.gray, width: 1)
}
.navigationBarTitle(note.title)
.padding()
}
}