SwiftUI

Im Frühjahr 2019 stellte Apple SwiftUI vor. Das ist eine neue Bibliothek, um grafische Benutzeroberflächen durch Swift-Code zusammenzusetzen. SwiftUI spart Millionen Maus- oder Trackpad-Bewegungen, die beim bisherigen Verfahren auf der Basis von UIKit erforderlich waren.

Mittlerweile ist SwiftUI einigermaßen praxistauglich. Zeit also, einen Blick auf neue Denkweisen und Paradigmen zu werfen! (Der präsentierte Code und die Screenshots beziehen sich auf Xcode 12 mit Swift 5.3.)

Update Juni 2022: Das mit der Praxistauglichkeit ist so eine Sache. Viele SwiftUI-Entwickler sind vom Konzept begeistert, aber stoßen früher oder später auf schwer (oder gar nicht) überwindbare Hürden. Lesen Sie unbedingt https://mjtsai.com/blog/2022/05/24/swiftui-in-2022/, bevor Sie Ihr erstes SwiftUI-Projekt starten!

Hello SwiftUI!

Um SwiftUI kennenzulernen, richten Sie ein neues Projekt vom Typ iOS/App ein und wählen im nächsten Dialog die folgenden Optionen: Interface = SwiftUI und Life Cycle = Swift UI App.

SwiftUI-Projekt einrichten

Xcode verwendet für das neue Projekt den folgenden Template-Code:

// Datei ContentView.swift
import SwiftUI

// beschreibt die Benutzeroberfläche
struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}

// nur für die Vorschau
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Dabei beschreibt die erste Struktur, die das Protokoll View implementiert, die Benutzeroberfläche der App. Die zweite Struktur (Protokoll PreviewProvider) ermöglicht eine Live-Vorschau der Oberfläche in Xcode. Die Vorschau gibt nicht nur ein optisches Feedback, sondern bietet weiterreichende Möglichkeiten:

  • Ein Klick auf ein Steuerelement der Vorschau markiert den entsprechenden Code.
  • cmd + Klick öffnet ein Kontextmenü, in dem Sie das markierte Element in einen Container verpacken oder seine Eigenschaften (Farben, Schriftgröße, Ausrichtung) einstellen können.
  • Der Button Live Preview ermöglicht es, die Oberfläche interaktiv auszuführen, ohne dazu gleich einen Simulator zu starten.
  • In einer Code-Datei darf es mehrere PreviewProvider geben, z.B. um unterschiedliche Komponenten getrennt voneinander zu testen.

Für den App-Start ist eine zweite Datei <projekname>App.swift zuständig, in der eine Instanz der ContentView-Klasse erzeugt wird:

// Datei <projektname>App.swift
import SwiftUI

// Programmstart
@main
struct hello_swiftuiApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Oberflächen zusammenstellen

Eigene Oberflächen setzen Sie zusammen, in dem Sie Text, TextField, Button und Instanzen diverser andere View-Strukturen erzeugen. Zur Anordnung verwenden Sie VStack– und HStack-Container, wobei deren Inhalt einfach durch einen Lambda-Ausdruck formuliert wird. Durch Parameter können Sie steuern, ob die Inhalte der Container zentriert, links- oder rechtsbündig bzw. oben oder unten ausgerichtet werden. Ein weiterer Parameter gibt den Standardabstand zwischen den Containerelementen an (z.B. VStack(alignment: .leading, spacing: 10) { ... }).

Nachgestellte Methoden steuern das Erscheinungsbild der Steuerelemente. Beispielweise bewirkt padding() ohne Parameter, dass das Steuerelement auf allen Rändern mit Freiraum umgeben wird. Dabei können Sie die Breite des Freiraums vom Betriebssystem vorgeben lassen oder selbst einen Wert angeben, wahlweise auch nur für einen bestimmten Rand (.padding(.bottom, 20)). .font stellt die gewünschte Schriftart ein, .background die Hintergrundfarbe usw.

Die folgenden Zeilen veranschaulichen die prinzipielle Vorgehensweise:

struct ContentView: View {
    var body: some View {
      // vertikaler Container, Inhalt im Lambda-Ausdruck
      VStack {
        Text("Hello, world!")
          .padding()                        // etwas Abstand
          .font(.headline)                  // »Headline«-Schriftart
        Text("More text")
          .padding(10)
          .border(Color.red, width: 3)      // rote Umrandung
        Text("Lorem ipsum dolor sit ... ")
          .frame(width: 200)                // mehrzeilig max. 200 pt breit
          .font(.caption)                   // »Caption«-Schrift
          .multilineTextAlignment(.center)  // zentriert
          .background(Color.yellow)         // Hintergrundfarbe

        // horiz. Container
        HStack {
          Image("arr-bend")                 // Bitmap aus Assets-Datei
            .resizable()                    // einstellbare Größe
            .frame(width: 64, height: 64)   // Größe der Bitmap
          Image("arr-horizontal-2")
            .resizable()
            .frame(width: 48, height: 48)
            .border(Color.green, width: 2)
          Image("arr-reset")
            .resizable()
            .frame(width: 32, height: 32)
            .background(Color.gray)
        }
      }
    }
}
Links SwiftUI-Code, rechts die Vorschau

Wenn Sie mehrere gleichartige Steuerelemente brauchen, können Sie den Code nicht einfach in einer Schleife formulieren:

HStack {
   // nicht erlaubt!!
   for i in 1...5 {
      Text("\(i)")
   }
}

Stattdessen müssen Sie ForEach zu Hilfe nehmen:

HStack {
   ForEach(1..<6) { i in 
      Text("\(i)")
   }
}

ForEach ist mehr als eine Variante zur Bildung von Schleifen! Vielmehr liefert ForEach eine Struktur, die View-Objekte erzeugt (siehe auch die leider sehr abstrakte offizielle Dokumentation und den deutlich verständlicheren StackOverflow-Artikel). Beachten Sie auch, dass ForEach ein Range-Objekt als Parameter erwartet. Ein ClosedRange-Objekt — z.B. 1...5 — wird nicht akzeptiert!

State Management

In UIKit können Sie im Code auf Eigenschaften von Steuerelementen zugreifen und diese ändern. Dieses Konzept gibt es in SwiftUI in dieser Form nicht. Die Daten sind inhärenter Teil eines Steuerelements. Wenn sich die Daten ändern, ändert sich auch der Inhalt des Steuerelements. Damit das funktioniert, müssen Sie allerdings speziell deklarierte Variablen verwenden.

Der einfachste Fall sind State-Variablen, die mit dem SwiftUI-Attribut @State deklariert werden. Dabei handelt es sich um üblicherweise private Variablen, die innerhalb einer Oberfläche bzw. eine Komponente einer Oberfläche einen Status speichern. Eine Veränderung von außen ist nicht vorgesehen.

Im folgenden Beispiel soll ein Klick auf einen Button einen Zähler um eins vergrößern. Der erforderliche Code sieht wie folgt aus. Bemerkenswert ist dabei die Zeile Text("counter=\(counter)"). Es besteht keine Notwendigkeit, dem Textfeld mitzuteilen, dass sich sein Inhalt verändert; dank der State-Variable bemerkt das Textfeld dies selbst und aktualisiert sich automatisch.

struct CounterView: View {
   @State private var counter = 0
   var body: some View {
      VStack(spacing: 10) {
         // verändert Counter
         Button(action: { counter += 1 }) {
            Text("Zähler + 1")
         }

         // zeigt den Inhalt von counter an
         Text("counter=\(counter)")

         // füllt den restlichen Platz, damit
         // sind Button + Text ganz oben
         Spacer()
      }
   }
}

In Xcode können Sie die CounterView in der Preview-Ansicht ausprobieren. Dazu klicken Sie auf den Button Live Preview.

In der »Live Preview« kann die Funktion des Buttons ausprobiert werden

Bei komplexeren Aufgabenstellungen befinden sich Ihre Daten außerhalb der SwiftUI-Oberfläche. (Es ist immer eine gute Idee, das Datenmodell und die Oberfläche zu trennen. Diese Grundregel gilt auch für SwiftUI-Apps.) Eine State-Variable reicht dann nicht mehr aus. Vielmehr muss es eine Möglichkeit geben, einen bidirektionalen Datenfluss zwischen der Oberfläche und Ihrem Datenmodell geben. SwiftUI stellt dazu unterschiedliche Konstrukte zur Verfügung:

  • Sie können Variablen (genaugenommen: Eigenschaften Ihres View-Objekts) mit dem Attribut @Binding kennzeichnen. Anders als bei @State ist das ist auch für öffentliche Eigenschaften erlaubt, d.h. eine Änderung von außen ist nun zulässig.

  • Alternativ können Sie eigene Klassen vom Protokoll ObservableObject ableiten. Das gibt Ihnen in der Folge die Möglichkeit, automatisiert auf Veränderungen der Daten zu reagieren.

Mehr Details, Beispiele sowie weitere Varianten sind hier beschrieben:

SwiftUI im Playground

Grundsätzlich ist es möglich, SwiftUI auch im Playground zu testen. Große Vorteile im Vergleich zu einem »richtigen« Testprojekt ergeben sich daraus nicht: Wenn Sie es dennoch probieren möchten, richten Sie einen neuen Playground für iOS ein und ersetzen den Mustercode durch die folgenden Zeilen:

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    var body: some View {
        Text("Hello World!")
    }
}

PlaygroundPage.current.setLiveView(ContentView())

Fünf Minuten im Playground mit einer Menge kryptischer Fehlermeldungen haben mich davon überzeugt, dass der Playground keine geeignete Spielwiese für SwiftUI ist. Stabilität und Geschwindigkeit des Playgrounds waren schon in der Vergangenheit nicht überzeugend, und daran hat sich mit Xcode 12 offensichtlich nichts geändert.

Licht und Schatten

So wie Swift Objective C abgelöst hat, wird SwiftUI das UIKit und die dazugehörenden Funktionen von Xcode ersetzen. Die Vorteile sind offensichtlich: Die Entwicklung von Oberflächen durch Swift-Code ist ungleich effizienter als deren klick-weise Zusammensetzung in Storyboards. Einmal entwickelte Komponenten einer Oberfläche können viel einfacher von einem Projekt in das nächste übernommen werden.

Das bedeutet aber nicht, dass das Arbeiten mit SwiftUI immer ein Vergnügen ist:

  • SwiftUI ist auch eineinhalb Jahre nach seiner Vorstellung noch kein vollständiger Ersatz für das UIKit. Beim Start eines neuen Projekts ist aktuell leider nur schwer einzuschätzen, ob SwiftUI bereits alle Funktionen enthält, die Sie brauchen.
  • Der Umstieg von UIKit auf SwiftUI ist nicht trivial. Das deklarative Modell erfordert nicht nur das Erlernen unzähliger neuer Klassen/Protokolle/Eigenschaften, sondern auch eine komplett neue Denkweise für den Umgang mit Daten.
  • SwiftUI führt rasch zu enorm verschachtelten Code mit unzähligen Ebenen von Lambda-Ausdrücken. Der resultierenden Klammernhölle entgehen Sie nur, wenn Sie Ihren Code von Anfang an in möglichst kompakte Teilfunktionen strukturieren.
  • Die Live-Vorschau und verwandte Xcode-Funktionen funktionieren in WWDC-Videos grandios, versagen in der Praxis aber rasch bei komplexerem Code. Sie ersparen sich zwar viele Klicks im Storyboard-Editor, müssen dafür aber ununterbrochen auf den Resume-Button des Vorschaufensters drücken. Für »richtige« Tests geht letztlich doch kein Weg vorbei an zeitaufwendigen Builds und Probeläufen im Simulator.

Dessen ungeachtet ist klar, dass SwiftUI die Zukunft der App-Programmierung im Apple-Universum ist. Interessanterweise beschreitet Google mit Jetpack Compose einen ähnlichen Weg. Diese in und für Kotlin entwickelte Bibliothek funktioniert ganz ähnlich wie SwiftUI. (Und bevor Sie zu überlegen beginnen, wer hier wen kopiert: Schon vor SwiftUI und Jetpack Compose gab es diverse andere deklarative Bibliotheken zur Gestaltung von Oberflächen! Am bekanntesten sind React und Flutter.)

Quellen

SwiftUI ist im Internet ausgezeichnet dokumentiert: