Refaktorierung in SwiftUI mit Property Wrappern und ViewModifiern​

Für SwiftUI gibt es einfache Refactoring-Methoden, um Code zu modularisieren und damit übersichtlicher und weniger fehleranfällig zu gestalten.​

In Pocket speichern vorlesen Druckansicht

(Bild: Shutterstock)

Lesezeit: 14 Min.
Von
  • Wolf Dieter Dallinger
Inhaltsverzeichnis

Views in Apples deklarativem GUI-Framework SwiftUI verwenden mit ViewBuilder eine domänenspezifische Sprache (DSL) für kompakten und leicht lesbaren Programmcode – vor allem im Vergleich zu dem älteren und imperativen GUI-Framework UIKit. Dennoch können Views aus vielen Codezeilen bestehen und unübersichtlich werden. Apples integrierte Entwicklungsumgebung Xcode bietet grundlegende Unterstützung für Refaktorierung in SwiftUI. Teile einer View in eine weitere View auszulagern, ist tägliches Handwerk.

Hilfreich sind zudem zwei weitere Ansätze. Der erste verwendet Property Wrapper für dynamische Eigenschaften. Schlüssel für AppStorage, Typ und Default-Wert lassen sich in einen eigenen Property Wrapper integrieren. Die Implementierung ist gekapselt und damit weniger fehleranfällig. Außerdem lässt sich der Property Wrapper einfacher in ein Framework auslagern.

Der zweite Ansatz sind ViewModifier mit einer zugehörigen View-Methode. Damit lassen sich unter anderem if-Abfragen analog zu bekannten ViewModifiern wie disabled() und hidden() gestalten und die dynamische Eigenschaft aus der View in den ViewModifier verlagern.

Verschiedene Apps, die einen Property Wrapper aus einem Framework einbinden, mögen unterschiedliche Default-Werte erfordern. Der folgende Text zeigt, wie man mit einem ViewModifier einen Standardwert vorgeben kann.

Alle Beispiele im Artikel müssen SwiftUI importieren, sofern nicht anders angegeben. Zur besseren Lesbarkeit fehlen die Import-Anweisungen in den Codeausschnitten.

In SwiftUI speichern dynamische Eigenschaften den Zustand der Benutzerschnittstelle. Wenn sich die gespeicherten Werte ändern, erfolgt eine Neuberechnung der Views. Animationen für Änderungen und Transitionen für die Ein- und Ausblendung einer View gestalten den Zustandswechsel grafisch ansprechend.

Für eine dynamische Eigenschaft ist ein Property Wrapper erforderlich, der zum Protokoll DynamicProperty konform ist. State, ObservedObject, AppStorage und Environment sind Beispiele für solche Property Wrapper aus SwiftUI.

Im folgenden Beispiel kann die App in einem gesperrten Modus laufen. Eine Mitarbeiterin bekommt vollen Zugriff, aber Besucher an einem Messestand können im gesperrten Modus nur eingeschränkt mit der App interagieren. Das ist zusammen mit dem Einzel-App-Modus oder dem geführten Zugriff eines Gerätes ein reales Messeszenario.

AppStorage ist der Property Wrapper für die dynamische Eigenschaft appIsLocked. Er speichert ihren Wert unter dem Schlüssel "appIsLocked" App-weit und dauerhaft in den UserDefaults. true ist der Default-Wert, der greift, wenn nil oder noch nichts unter dem Schlüssel in den UserDefaults gespeichert ist. Die App ist beim ersten Start dadurch gesperrt, also Secure-by-Design.

Wenn appIsLocked den Wert true hat, zeigt die App einen Hinweis auf die gesperrte App.

struct MyView: View {
  @AppStorage("appIsLocked") 
     private var appIsLocked = true
    
  var body: some View {
    if appIsLocked {
      Text("Die App ist gesperrt.")
    }
  }
}

Das einfache Beispiel erfordert keine Refaktorierung. In einem App-Projekt mögen aber Gründe dafür vorliegen.

Der Zugriff per AppStorage auf einen bestimmten Wert in den UserDefaults findet häufig an mehreren Stellen in einer App statt. Wenn die App in den gesperrten Modus wechselt, muss sie viele Buttons, Menüs und Views sperren. Der Schlüssel und der Default-Wert sind an allen betroffenen Stellen erforderlich. Damit ergeben sich viele Fehlerquellen.

Wer einen eigenen Property Wrapper verwendet, kann beides auslagern und damit kapseln. Ein eigener Property Wrapper verbessert zudem die Lesbarkeit. Das gilt umso mehr, wenn die App mehrere dynamische Eigenschaften in einer View verwendet.

Der erste Ansatz zur Refaktorierung ist daher ein eigener Property Wrapper namens AppIsLocked:

struct MyView: View {
  @AppIsLocked private var appIsLocked
    
  var body: some View {
    if appIsLocked {
      Text("Die App ist gesperrt.")
    }
  }
}

Der zweite Ansatz betrifft sowohl die dynamische Eigenschaft als auch die if-Abfrage. Wird – wie im Beispiel – in einer View die dynamische Eigenschaft nur für die if-Abfrage verwendet, lassen sich beide in einen eigenen ViewModifier auslagern und mit einer Methode aus einer View-Extension anwenden. Hier heißt die Methode ifAppIsLocked(). Das erhöht die Lesbarkeit des Programmcodes zusätzlich:

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

Das Refaktorieren hat den kompletten Overhead entfernt, und es bleibt klar lesbarer Code übrig: Zeige diesen Text an, wenn die App gesperrt ist. Erneut lohnt sich die Refaktorierung, wenn ein Projekt mehrere solche if-Abfragen enthält.

Benötigt man eine Funktionsweise in mehreren Projekten, bietet es sich an, ein Framework zu erstellen und in ein Package zu integrieren. Eigene Property Wrapper und ViewModifier mit View-Extensions erleichtern das Auslagern in ein Framework.

Je nach Projekt sind unterschiedliche Default-Werte für einen Property Wrapper aus einem Framework erforderlich. Eine App soll beim ersten Start vielleicht im sicheren Modus laufen, der für eine andere womöglich nur eine ergänzende Option ist. Der Text zeigt im Folgenden als dritten Fall, wie man mit einem ViewModifier und einer View-Extension einen Default-Wert für einen eigenen Property Wrapper festlegt, der AppStorage verwendet. Den Default-Wert registriert der Code dazu in den UserDefaults.

Der Aufruf der entsprechenden View-Methode ist nur einmal pro Start der App erforderlich. Er muss ausreichend hoch in der View-Hierarchie erfolgen, beispielsweise in der App-Struktur:

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