Refaktorierung in SwiftUI mit Property Wrappern und ViewModifiern​

Seite 3: ViewModifier und View-Extension

Inhaltsverzeichnis

Ein ViewModifier ist eine Struktur, die eine View erzeugt. Der Unterschied zur reinen View liegt in der Semantik: Der ViewModifier erstellt keine komplett neue View, sondern modifiziert eine vorhandene.

Ziel ist ein ViewModifier, der eine andere View genau dann anzeigt, wenn die App gesperrt ist. Ein passender Name ist das oben vorgeschlagene IfAppIsLocked.

Der ViewModifier benötigt eine dynamische Eigenschaft, die den gewünschten Zustand darstellt. Dafür dient der zuvor erstellte Property Wrapper AppIsLocked:

struct IfAppIsLocked: ViewModifier {
  @AppIsLocked private var appIsLocked

  func body(content: Content) -> some View {
    if appIsLocked {
      content
    }
  }
}

Eine App wendet den ViewModifier mit der View-Methode modifier() auf eine View an. MyView im Beispiel verwendet die dynamische Eigenschaft appIsLocked nur für die if-Abfrage. Da diese in den ViewModifier gewandert ist, benötigt MyView appIsLocked nicht mehr. Das Beispiel fällt mit dem ViewModifier deutlich kürzer aus:

struct MyView: View {
  var body: some View {
    Text("Die App ist gesperrt.")
         .modifier(IfAppIsLocked())
  }
}

Üblicherweise lagert man den Aufruf von modifier() in eine View-Extension aus und verbessert die Lesbarkeit noch weiter:

extension View {
  func ifAppIsLocked() -> some View {
    modifier(IfAppIsLocked())
  }
}

Mit der neuen View-Methode ifAppIsLocked() sieht der Code folgendermaßen aus:

struct MyView: View {
  var body: some View {
    Text("Die App ist gesperrt.")
         .ifAppIsLocked()
  }
}

Das Ziel ist erreicht: Der komplette Overhead ist wegrefaktoriert und es bleibt klar lesbarer Code übrig: Zeige den passenden Text an, wenn die App gesperrt ist.

In ein Framework ausgelagerte Property Wrapper und ViewModifier sind für mehrere Projekte wiederverwendbar. Ein Projekt kann erfordern, dass die App nach Installation gesperrt ist. Ein anderes ermöglicht eine Sperre als Option und startet die App nach der Installation entsperrt.

Um je nach Projekt unterschiedliche Default-Werte zuzulassen und trotzdem denselben Property Wrapper über ein Framework zur Verfügung zu stellen, bietet sich an, den Default in jedem Projekt per ViewModifier vorzugeben. Ohne die Vorgabe sollte die Anwendung automatisch einen sicheren Default-Wert nehmen, damit der Property Wrapper das Paradigma Secure-by-Design erfüllt.

Umgebungsvariablen stellen eine gute Methode dar, einen Wert über mehrere Views hinweg an eine View oder einen Property Wrapper für dynamische Eigenschaften weiterzureichen.

Default-Werte für UserDefaults und damit den Property Wrapper AppStorage lassen sich direkt in den UserDefaults registrieren. Dieses Vorgehen ist einfacher und daher zu bevorzugen.

Einen in den UserDefaults angegebenen Standardwert für einen Schlüssel gibt die App bis zum nächsten Start immer dann zurück, wenn zu einem Schlüssel kein Wert oder nil gespeichert ist.

Die Registrierung sollte stattfinden, bevor die App den Wert das erste Mal liest. Die Ausführung der ViewModifier zu onAppear und task erfolgt zu spät, aber der Initialisierer eines ViewModifier wird vor der ersten Berechnung des ihm folgenden restlichen View-Tree ausgeführt.

Der ViewModifier AppIsUnlockedByDefault und die zugehörige View-Methode appIsUnlockedByDefault() sehen damit folgendermaßen aus:

struct AppIsUnlockedByDefault: ViewModifier {
  init() {
    UserDefaults.standard.register(defaults: [
      "appIsLocked": false,
    ])
  }
    
  func body(content: Content) -> some View {
    content
  }
}

extension View {
  func appIsUnlockedByDefault() -> some View {
    modifier(AppIsUnlockedByDefault())
  }
}

Wichtig ist, den neuen Default-Wert vor dem ersten Verwenden des Property Wrapper AppIsLocked zu setzen. Ein geeigneter Ort ist an der ersten View.

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
        .appIsUnlockedByDefault()
    }
  }
}