22. Jul. 2020
Die Vorteile von Modularisierung sind allseits bekannt. Durch die Aufteilung einer großen Einheit in einzelne Module können Komponenten beispielsweise erneut verwendet werden, wodurch bei UI-Komponenten sowohl die Arbeitszeit reduziert als auch das Design vereinheitlicht werden kann. Darüber hinaus erleichtert eine logische Trennung einzelner Komponenten die Einarbeitung in eine neue Projektstruktur und führt dazu, dass einzelne Komponenten einfacher getestet werden können. Letzteres ist insbesondere in der Softwareentwicklung von bedeutender Relevanz.
Doch wie kann ein theoretisches Konzept der Modularisierung in der Praxis, genauer gesagt in der Programmiersprache Swift für iOS-Applikationen umgesetzt werden?
Wir haben’s ausprobiert – anhand einer selbst entwickelten iOS Wetter-App. Die hierfür verwendeten Wetterdaten werden von der OpenWeatherMap API angefragt, aufbereitet und anschließend dargestellt.
Um das UI zu erstellen, wurde sich für die auf der WWDC 2019 vorgestellte Technologie SwiftUI entschieden. Hierbei handelt es sich um einen deklarativen Ansatz, der UI Zustände in Swift beschreibt. Das Layouting wird von der SwiftUI Rendering Engine übernommen. In der Geschäftslogik wird das Combine Framework verwendet, welches einen reaktiven Programmieransatz verwendet und sich sehr gut in SwiftUI integrieren lässt. In dieser Kombination lassen sich mit SwiftUI native Anwendungen für IOS, macOS, watchOS und tvOS einfach und effektiv programmieren.
Nach der Erstellung eines neuen SwiftUI Projekts, erhält man zunächst folgende Ansicht:
Zur Verbesserung der Projektstruktur, wird im nächsten Schritt ein Workspace erstellt und das bisherige Projekt hinzugefügt. Dies hat den Vorteil, dass wir sowohl das Projekt als auch alle benötigten Module in einem Workspace bearbeiten können. Um die Geschäftslogik und die UI-Komponenten zu definieren, muss anschließend das Swift Package „WDYWeatherCore“ geschaffen werden. In diesem Package finden sich wichtige Ordner für die zwei Module „WDYWeatherCore“ und „WDYWeatherViews“, wodurch folgende Projektstruktur entsteht:
Danach müssen die Module „WDYWeatherCore“ und „WDYWeatherViews“ noch als Frameworks im „WDYWeather“ Target hinzugefügt werden, sodass eine Projektstruktur entsteht, die in der weiteren Entwicklung viele Vorteile hat:
Zum einen ist die „Hürde“ für die Erstellung von weiteren Modulen nun sehr gering, sodass eine logische Gliederung sowie die Wiederverwendung von Modulen einfach und mit wenig Aufwand möglich sind. Diese Wiederverwendbarkeit ist dann zwingend erforderlich, wenn es sich um eine iOS Share Extension oder macOS, watchOS, tvOS App handelt, da diese in Xcode als neue „Targets“ anzusehen sind und dadurch eine eigene Verzeichnisstruktur besitzen. Zum anderen können einzelne Module nun problemlos größer und ggf. in ein eigenständiges Paket ausgelagert werden. So ist auch eine Wiederverwendbarkeit bei weiteren Projekten möglich.
Das „WDYWeatherCore“ Modul enthält die Geschäftslogik der App. Sie besteht in diesem Projekt aus der Abfrage und Aufbereitung von Wetterdaten. Hierfür werden Swift Structures erstellt, welche die Rückgabedaten der API repräsentieren. Sie können mit Hilfe der Web-App unkompliziert aus den JSON Wetterdaten der API generiert werden. Um zwischen den Modulen eine logische Trennung zu erhalten, werden diese Datentypen mit einem Access Level internal deklariert. Dies bedeutet, dass die Daten der API innerhalb des „WDYWeatherCore“ Moduls verarbeitet und aufbereitet werden sollen. Als Schnittstelle für die App wird die „WeatherModel“ Structure erstellt. Sie enthält alle wichtigen Daten, die in der App benötigt werden.
WeatherModel.swift
public struct WeatherModel {
public let location: Stringpublic let current: Datapoint?public let forecast:[Datapoint]}
Ein Datenpunkt besteht hierbei aus einem Datum, der Temperatur, dem Luftdruck, der Luftfeuchtigkeit und einem Enum, dass eine Zusammenfassung des Wetters enthält wie z.B. „sonnig“, „bewölkt“, „regnerisch“, „stürmisch“. Diese Zusammenfassung wird beispielsweise für die Darstellung der Wetter-Symbole verwendet.
Für die Anfrage der Wetterdaten kommt das Combine Framework zum Einsatz. Wir erstellen jeweils einen Publisher, der die aktuellen Wetterdaten und die Wettervorschau bei der API anfragt. Nun kann eines der Kernfeature von Combine genutzt werden. Es soll erst dann eine Rückgabe erfolgen, wenn beide Publisher ein Ergebnis geliefert haben und diese Ergebnisse aufbereitet wurden. Anstatt hier klassisch mit beispielsweise einer DispatchSemaphore zu arbeiten, kann der soeben beschriebene Zusammenhang mit dem Combine Framework reaktiv ausdrückt werden.
WeatherAPI.swift
public func getData(locationName: String = "Oldenburg") -> AnyPublisher<WeatherModel, APIError> {
return getForecastData(locationName: locationName)
// 1
.zip(getCurrentData(locationName: locationName))
// 2
.tryMap { (forecast, current) in
try self.mapToWeatherModel(forecast, current, for: locationName)
}
// 3
.mapError { inputError in
if let inputError = (inputError as? APIError) {
return inputError
} else {
return .network(description: inputError.localizedDescription)
}
// 4
.eraseToAnyPublisher()
}
Zunächst (1) kombinieren wir die Ergebnisse der beiden API Publisher. Anschließend (2) wird aus den Daten das „WeatherModel“ erstellt. Es folgt (3) eine Fehlerbehandlung möglicher fehlerbehafteter Antworten der API. Abschließend (4) ist es bei der Nutzung des Combine Frameworks üblich, das als Publisher Datentyp ein „AnyPublisher<Output, Failure>“ zurückgegeben wird.
Das dadurch erzeugte „WeatherModel“ enthält nun alle relevanten Daten für unser UI. Bei der Implementierung ist insbesondere auf die Modularisierung zu achten, sodass keine Abhängigkeiten zu einem UI Framework wie beispielsweise AppKit, UIKit oder SwiftUI erzeugt werden. Dadurch lässt sich diese Geschäftslogik nicht nur in einer iOS App, sondern auch in einer nativen macOS App verwenden.
Für die Implementierung einer iOS App mit SwiftUI bietet sich die Verwendung eines Model View ViewModel (MVVM) Ansatzes an. Dadurch, dass die App eine Liste der Wettervorschaudaten enthalten soll, kann diese Liste möglicherweise auch in einer watchOS App verwendet werden. Um dies zu ermöglichen, muss das Zellendesign in das „WDYWeatherViews“ Modul integriert werden – dies gelingt wie folgt:
Zunächst wird das „ForecastItemViewModel“, welches alle anzuzeigenden Daten enthält, erstellt. Die Aufbereitung der einzelnen Daten findet hierbei in dem ViewModel statt, so wird z.B. der Zeitstempel in das gewünschte Format konvertiert. Anschließend wird die „ForecastItemView“ erstellt, welche das ViewModel enthält. Dort können mit Hilfe von SwiftUI die Texte und Bilder ausgerichtet werden. Die daraus entstandene Ansicht kann mit Hilfe eines „PreviewProviders“ angesehen und ggf. feinjustiert werden.
Nachdem die Geschäftslogik und die UI-Komponenten bisher getrennt voneinander entwickelt wurden und keinerlei Berührungspunkte hatten, kann nun die Implementierung der iOS App erfolgen. Hierfür wird unser zuvor angelegtes Projekt aufgerufen und die „WeatherView“ erstellt. Sie enthält alle wichtigen Komponenten.
Neben der deklarativen Herangehensweise bietet SwiftUI noch weitere Vorteile bei der Entwicklung, die für eine enorme Zeitersparnis sorgen. So kann bspw. eine Vorschau der zu bearbeitenden Ansicht live in der Entwicklungsumgebung Xcode angezeigt werden. Anpassungen an dem UI können dadurch einfach und effizient vorgenommen werden, ohne dass ein Entwickler die App neu öffnen und in die bearbeitete Ansicht navigieren muss.
Die „forecastView“ Variable enthält hierbei die zuvor erzeugte „ForcastItemView“ Komponente aus dem „WDYWeatherViews“ Modul. Für die Anpassung an weitere Apple-Geräte, wie z.B. das iPad, musste in diesem simplen UI lediglich die Breite der Vorschau-Liste auf einen maximalen Wert von 500 Punkten begrenzt werden. So erhalten wir eine voll funktionsfähige Wetter-App, die für alle mobilen Endgeräte-Größen die verfügbaren Wetterdaten über die API anfragt.
private var forecastView: some View {
List(viewModel.forecastItems) { item in
ForecastItemView(viewModel: item)
}
.listSeparatorStyleNone()
.frame(maxWidth: 500, maxHeight: 300, alignment: .center) // set maxWidth for iPad
.cornerRadius(8)
}
Mithilfe der durch den Swift Package Manager eingeführten Paketstruktur und der Integration in Xcode lässt sich eine logische Zerteilung von Komponenten in Swift realisieren. Sind die Grundlagen in einer Projektstruktur erst einmal geschaffen, können weitere Module einfach ergänzt werden.
Die beschriebenen Vorteile sind offensichtlich: Durch die Entwicklung in Komponenten wird schon bei der Implementierung der Fokus auf die Wiederverwendbarkeit gelegt. Durch eine klare Projektstruktur finden sich nicht nur neue Kollegen schneller im Projekt zurecht, auch profitiert die Arbeit am Projekt durch kürzere Compile-Zeiten.
Wir haben gesehen, dass mit SwiftUI auch UI-Komponenten in einzelne Module verpackt werden können. So kann bspw. ein Corporate Design auch in einer Codebasis aufgebaut werden. Einmal entwickelte und designte UI-Komponenten lassen sich anschließend beliebig erneut verwenden. Trotz Wiederverwendbarkeit gibt es bei der aktuellen Version von Swift (5.2) allerdings noch ein Hemmnis, denn: Aktuell ist es noch nicht möglich weitere Ressourcen wie bspw. xib Dateien, Storyboards oder auch Bilder über ein Modul bereitzustellen. Hierfür wurden allerdings bei der Swift Evolution bereits die Proposals 0271 und 0278 eingereicht und implementiert, sodass diese Einschränkung mit Swift 5.3 aufgehoben wird.
Um die Verwendung der erstellten Komponenten auch anhand anderer Systeme zu verdeutlichen, werden wir in den folgenden Blogartikeln näher auf die Erstellung von Apps für macOS, watchOS und tvOS eingehen.