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.