SwiftUI in der Praxis, Teil 1

Seite 3: Integration von UIKit Views

Inhaltsverzeichnis

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 von UIViewController-Instanzen in SwiftUI.
  • NSViewControllerRepresentable für die Verwendung von NSViewController-Instanzen in SwiftUI.
  • NSViewRepresentable für den Einsatz von NSView-Instanzen in SwiftUI.
  • WKInterfaceObjectRepresentable für den Einsatz von WKInterfaceObject-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()
    }
}

UITextView aus dem UIKit-Framework lässt sich nun innerhalb von SwiftUI einsetzen (Abb. 3).