Integration von SwiftUI in UIKit
Die Voraussetzungen sind also erfüllt — doch wie kann nun mit der Migration begonnen werden? Glücklicherweise bietet SwiftUI Möglichkeiten einzelne Einheiten (etwa einen Screen oder auch nur ein Bedienelement) die in SwiftUI geschrieben sind in UIKit-Code einzubinden. Dadurch ist es möglich den alten UIKit-Code Schritt für Schritt auf SwiftUI zu migrieren oder aber vorerst nur neue Interface-Elemente in SwiftUI zu implementieren.
Um ein erstes Gefühl für SwiftUI zu bekommen, suchen wir uns einen bestehenden simplen View-Controller zur Migration
aus: den TextViewController. Dessen Aufgabe ist es Informationen wie Impressum, Datenschutzbestimmungen und
Lizenzvereinbarung bestehend aus einer Überschrift und einem mehrzeiligen Text anzuzeigen.
Als Erstes wird eine neue Datei TextView.swift angelegt, in der die View entworfen werden kann. In einem VStack, der
die enthaltenen Elemente vertikal aneinander ausgerichtet, wird ein HStack bestehend aus dem cronn-Logo und einem
Label (Text) platziert. Durch den Parameter alignment: .leading des VStack werden die enthaltenen Elemente je nach
eingestellter Leserichtung ausgerichtet. Der Text wird durch sogenannte ViewModifier in seiner Erscheinung angepasst.
Der lineLimit(1) ViewModifier sorgt dafür, dass der Text einzeilig, also ohne Zeilenumbrüche, dargestellt wird.
Mittels font(.title) wird der Text im Standard iOS-Titel-Stil angezeigt. Der eigentliche Informationstext wird in
einer ScrollView unterhalb der Kopfzeile angezeigt. Als letztes Element wird ein Button eingefügt, der bei Benutzung
eine der View übergebene dismissAction()-Methode aufruft. Dieser besteht lediglich aus dem Text „OK”. Damit die
Beschriftung zentriert positioniert wird, werden vor und nach diesem noch jeweils ein Spacer eingefügt. Schließlich
erzeugt der padding() ViewModifier am VStack etwas Abstand zu den enthaltenen Elementen.
import SwiftUI
struct TextView: View {
@ObservedObject var textStore: TextStore
var dismissAction: (() -> Void)?
var body: some View {
VStack(alignment: .leading) {
HStack() {
Image("cronnGreyscale")
Text(textStore.displayText.title)
.lineLimit(1)
.font(.title)
}
ScrollView {
Text(textStore.displayText.body)
}
Button(action: {
(self.dismissAction ?? {})()
}) {
Spacer()
Text("OK")
Spacer()
}
}.padding()
}
}
Entsprechend des bei SwiftUI genutzten Design-Patterns Model-View-ViewModel (MVVM) legen wir auch ein
ViewModel TextStore an, welches auf das bereits bestehende Model DisplayText zugreift. In diesem einfachen Fall hat
das ViewModel nur die Funktion Änderungen am Model zu überwachen und die View mittels @Published-Property-Wrapper über
solche zu informieren. Würde sich zwischenzeitlich also der im Model gespeicherte Text ändern, würde die View
automatisch neu gezeichnet. Dies wird im aktuellen Beispiel nicht passieren, ist aber für andere Anwendungsfälle sehr
praktisch.
class TextStore: ObservableObject {
@Published var displayText: DisplayText
init(displayText: DisplayText) {
self.displayText = displayText
}
}
struct DisplayText: Codable {
var title: String
var body: String
}
Doch welche Möglichkeiten gibt es zur Orientierung beim Entwurf einer View? In der UIKit-Welt gibt es Storyboards
oder .xib-Dateien in denen Views grafisch entworfen und zusammengeklickt werden können. Damit kann bereits vor dem
Testen auf dem Simulator oder Gerät ein Eindruck von der Benutzeroberfläche gewonnen werden. SwiftUI sieht dafür
Previews vor, die in der gleichen Datei wie die View implementiert werden. Diese ermöglichen es neben dem Code eine
grafische Vorschau einzublenden. Dafür erstellen wir in der previews-Property eines PreviewProvider eine
entsprechende View mit Beispieldaten. Ändert sich etwas im Code an der View, wird die Vorschau automatisch aktualisiert.
Seit Xcode 14 können die Previews mittels previewDevice(_:) ViewModifier auch auf verschiedenen Gerätetypen angezeigt
werden.
struct TextView_Previews: PreviewProvider {
static var previews: some View {
TextView(textStore:
TextStore(displayText:
DisplayText(
title: "Lorem ipsum dolor sit er elit lamet, consectetaur",
body: """
Lorem ipsum dolor sit amet, consetetur sadipscing
elitr, sed diam nonumy eirmod tempor invidunt ut
labore et dolore magna aliquyam erat, sed diam
...
Lorem ipsum dolor sit amet,
"""
)
)
)
.previewDevice(PreviewDevice(rawValue: "iPhone 8"))
.previewDisplayName("iPhone 8")
}
}
Wie kann nun die TextView in das bestehende Projekt integriert werden, um den alten TextViewController zu ersetzen?
Dafür gibt es den UIHostingController, welcher es erlaubt eine beliebige SwiftUI-View in einen UIKit-View-Controller
einzubetten. Dem Initializer des View-Controllers übergeben wir dabei das ViewModel vom Typ TextStore, dass wiederum
benötigt wird um die TextView zu initialisieren. Mittels super.init(rootView: textView) initialisieren wir dann den
View-Controller mit der SwiftUI-View als rootView. Schließlich kommt noch die dismissAction() zum Tragen. Sollte der
OK-Button getapt werden, lassen wir sich den View-Controller selber ausblenden.
import SwiftUI
class TextViewController: UIHostingController<TextView> {
init(textStore: TextStore) {
let textView = TextView(textStore: textStore)
super.init(rootView: textView)
rootView.dismissAction = {
self.dismiss(animated: true, completion: nil)
}
}
@objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Je nachdem wie der ursprüngliche View-Controller implementiert und genutzt wurde müssen nun noch kleinere Anpassungen
vorgenommen werden. Im Falle der „Image Recognition Playground”-App wurde der TextViewController bisher programmatisch
und nicht etwa via Storyboard-Segues präsentiert. Er wurde jedoch bisher über das Storyboard initialisiert, was nun auch
programmatisch geschieht. Dafür wird die folgende Änderung vorgenommen.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let textVC = storyboard.instantiateViewController(
withIdentifier: "TextVC")
as! TextViewController
textVC.displayText = displayText
self.present(textVC, animated: true, completion: nil)
let textStore = TextStore(displayText: displayText)
let textVC = TextViewController(textStore: textStore)
self.present(textVC, animated: true, completion: nil)
Als letztes kann nun der TextViewController aus dem Storyboard entfernt werden. Im nächsten Post der Serie wird es
darum gehen wie UIKit-Code in SwiftUI integriert und der Einstiegspunkt der App auf eine SwiftUI-View gesetzt werden
kann.