SwiftUI-Interaktion mit Redux

Bild

Hallo alle zusammen. In diesem Artikel werden wir über das SwiftUI- Framework in Verbindung mit Redux sprechen. Mit diesem Bundle können wir schnell und einfach Anwendungen erstellen. SwiftUI wird im Gegensatz zu UIKit zum Erstellen einer Benutzeroberfläche im deklarativen Stil verwendet . Redux dient wiederum dazu, den Status der Anwendung zu steuern.

State ist ein grundlegendes Konzept in SwiftUI und Redux. In unserem Fall ist dies nicht nur ein Schlagwort, sondern auch eine Einheit, die sie verbindet und es ihnen ermöglicht, sehr gut zusammenzuarbeiten. In diesem Artikel werde ich versuchen zu zeigen, dass die obige These wahr ist, also fangen wir an!

Bevor wir uns mit dem Schreiben von Code befassen, wollen wir zunächst verstehen, was Redux ist und woraus es besteht.

Redux ist eine Open Source-Bibliothek zum Verwalten des Status einer Anwendung. Wird am häufigsten in Verbindung mit React oder Angular verwendet, um die Client-Seite zu entwickeln. Enthält eine Reihe von Tools, um die Übertragung von Speicherdaten durch den Kontext erheblich zu vereinfachen. Schöpfer: Daniil Abramov und Andrew Clark.

Für mich ist Redux nicht nur eine Bibliothek, es ist bereits etwas mehr, ich schreibe es den Architekturentscheidungen zu, auf denen die Anwendung basiert. In erster Linie aufgrund seines unidirektionalen Datenstroms.

Multidirektionaler oder unidirektionaler Fluss


Um zu erklären, was ich unter Datenfluss verstehe, werde ich das folgende Beispiel geben. Eine mit VIPER erstellte Anwendung unterstützt einen multidirektionalen Datenstrom zwischen Modulen:

Bild

Redux wiederum ist ein unidirektionaler Datenstrom und lässt sich anhand seiner Bestandteile am einfachsten erklären.

Bild

Lassen Sie uns ein wenig detailliert über jede Redux-Komponente sprechen.

Der Staat ist die einzige Quelle der Wahrheit, die alle notwendigen Informationen für unsere Anwendung enthält.

Aktion ist die Absicht, den Zustand zu ändern. In unserem Fall ist dies eine Aufzählung, die neue Informationen enthält, die wir im aktuellen Status hinzufügen oder ändern möchten.

ReduzierstückIst eine Funktion, die Aktion und aktuellen Status als Parameter verwendet und einen neuen Status zurückgibt. Dies ist der einzige Weg, um es zu erstellen. Es ist auch erwähnenswert, dass diese Funktion sauber sein sollte.

Store ist ein Objekt, das State enthält und alle erforderlichen Tools zum Aktualisieren bereitstellt.

Ich glaube, dass die Theorie ausreicht, lassen Sie uns zur Praxis übergehen.

Redux-Implementierung


Eine der einfachsten Möglichkeiten, ein Tool kennenzulernen, besteht darin, es zu verwenden, wie mein Programmierlehrer sagte. Wenn Sie eine Programmiersprache lernen möchten, schreiben Sie eine Anwendung darauf. Erstellen wir also eine kleine Anwendung, lassen Sie es ein einfaches Trainingstagebuch sein, es gibt nur vier Optionen: Die erste dient zum Anzeigen einer Liste von Trainingseinheiten, die zweite zum Hinzufügen eines abgeschlossenen Trainings, die dritte zum Löschen und die vierte zum Sortieren von Trainingseinheiten. Ziemlich einfache Anwendung, aber gleichzeitig können wir uns mit Redux und SwiftUI vertraut machen.

Erstellen Sie ein sauberes Projekt in Xcode, geben Sie ihm den Namen WorkoutsDiary und wählen Sie vor allem SwiftUI für die Benutzeroberfläche aus, da wir SwiftUI zum Erstellen unserer Benutzeroberfläche verwenden werden.

Nach dem Erstellen des Projekts. Erstellen Sie eine Trainingsstruktur, die für das von uns abgeschlossene Training verantwortlich ist.

import Foundation

struct Workout: Identifiable {
    let id: UUID = .init()
    let name: String
    let distance: String
    let date: Date
    let complexity: Complexity
}

Wie Sie sehen können, ist an dieser Struktur nichts Verrücktes, das ID-Feld muss dem identifizierbaren Protokoll entsprechen, und das Komplexitätsfeld ist nur eine Aufzählung mit der folgenden Definition:

enum Complexity: Int {
    case low
    case medium
    case high
}

Nachdem wir nun alles haben, was wir für die Implementierung von Redux benötigen, erstellen wir zunächst einen Status.

struct AppState {
    var workouts: [Workout]
    var sortType: SortType?
}

State ist eine einfache Struktur, die zwei Felder enthält: Workouts und sortType . Das erste ist eine Liste von Workouts und das zweite ist ein optionales Feld, das bestimmt, wie die Liste sortiert wird.

SortType ist eine Aufzählung, die wie folgt definiert ist:

enum SortType {
    case distance
    case complexity
}

Der Einfachheit halber werden wir nach Entfernung und Schwierigkeit in absteigender Reihenfolge sortieren, dh je höher die Komplexität unseres Trainings ist, desto höher wird es in unserer Liste angezeigt. Es ist erwähnenswert, dass sortType ein optionaler Typ ist und null sein kann, was bedeutet, dass die Liste im Moment nicht sortiert ist.

Wir werden die Implementierung unserer Komponenten fortsetzen. Lassen Sie uns eine Aktion erstellen

enum Action {
    case addWorkout(_ workout: Workout)
    case removeWorkout(at: IndexSet)
    case sort(by: SortType)
}

Wie wir sehen können, ist Action eine Aufzählung mit drei Fällen, die uns die Möglichkeit geben, unseren Staat zu manipulieren .

  • addWorkout (_ Workout: Workout) fügt einfach ein Workout hinzu, das als Parameter übergeben wird.
  • removeWorkout (at: IndexSet) entfernt das Element am angegebenen Index.
  • sort (nach: SortType) sortiert die Trainingsliste nach dem angegebenen Sortiertyp .

Lassen Sie uns eine der komplexesten Komponenten erstellen, nämlich Reducer :

func reducer(state: AppState, action: Action) -> AppState {
    var state = state
    
    switch action {
    case .addWorkout(let workout):
        state.workouts.append(workout)
        
    case .removeWorkout(let indexSet):
        state.workouts.remove(atOffsets: indexSet)
    
    case .sort(let type):
        switch type {
        case .distance:
            state.workouts.sort { $0.distance > $1.distance }
            state.sortType = .distance
        case .complexity:
            state.workouts.sort { $0.complexity.rawValue > $1.complexity.rawValue }
            state.sortType = .complexity
        }
    }
    return state
}


Die Funktion, die wir geschrieben haben, ist recht einfach und funktioniert wie folgt:

  1. Kopiert den aktuellen Status , um damit zu arbeiten.
  2. Basierend auf Action aktualisieren wir unseren kopierten Status .
  3. Geben Sie den aktualisierten Status zurück

Es ist erwähnenswert, dass die obige Funktion eine reine Funktion ist, und das wollten wir erreichen! Eine Funktion muss zwei Bedingungen erfüllen, um als „rein“ zu gelten:

  • Jedes Mal gibt die Funktion dasselbe Ergebnis zurück, wenn sie mit demselben Datensatz aufgerufen wird.
  • Es gibt keine Nebenwirkungen.

Das letzte fehlende Redux-Element ist der Store . Implementieren wir es also für unsere Anwendung.

final class Store: ObservableObject {
     @Published private(set) var state: AppState
     
     init(state: AppState = .init(workouts: [Workout]())) {
         self.state = state
     }
     
     public func dispatch(action: Action) {
         state = reducer(state: state, action: action)
     }
 }

In Implementierungen des Store- Objekts nutzen wir alle Vorteile des ObservableObject- Protokolls , wodurch das Schreiben einer großen Menge von Vorlagencode oder die Verwendung von Frameworks von Drittanbietern ausgeschlossen werden kann. Die State- Eigenschaft ist schreibgeschützt und verwendet den Wrapper der @ Publishing- Eigenschaft. Dies bedeutet, dass SwiftUI bei jeder Änderung Benachrichtigungen erhält. Die init-Methode nimmt einen Anfangszustand als Parameter mit einem bestimmten Standardwert in Form eines leeren Arrays von Workout-Elementen an. Die Versandfunktion ist die einzige Möglichkeit, den Status zu aktualisieren: Sie ersetzt den aktuellen Status durch den neuen Status, der von der Reduzierungsfunktion basierend auf erstellt wurdeAktion , die als Parameter übergeben wird.

Nachdem wir alle Komponenten von Redux implementiert haben, können wir mit der Erstellung einer Benutzeroberfläche für unsere Anwendung beginnen.

Anwendungsimplementierung


Die Benutzeroberfläche unserer Anwendung wird recht einfach sein. Und es wird aus zwei kleinen Bildschirmen bestehen. Der erste und Hauptbildschirm ist ein Bildschirm, auf dem eine Liste der Trainingseinheiten angezeigt wird. Der zweite Bildschirm ist der Bildschirm zum Hinzufügen von Training. Außerdem wird jedes Element in einer bestimmten Farbe angezeigt. Die Farbe spiegelt die Komplexität des Trainings wider. Die roten Blutkörperchen zeigen die höchste Schwierigkeit des Trainings an, Orange ist für die durchschnittliche Schwierigkeit verantwortlich und Grün ist für das einfachste Training verantwortlich.

Wir werden die Schnittstelle mit einem neuen Framework von Apple namens SwiftUI implementieren. SwiftUI ersetzt unser bekanntes UIKit. SwiftUI unterscheidet sich grundlegend von UIKit, vor allem darin, dass es ein deklarativer Ansatz zum Schreiben von UI-Elementen mit Code ist. In diesem Artikel werde ich nicht auf alle Feinheiten von SwiftUI eingehen, und ich gehe davon aus, dass Sie bereits Erfahrung mit SwiftUI haben. Wenn Sie keine Kenntnisse über SwiftUI haben, empfehle ich Ihnen, die Dokumentation von Apple zu beachten, nämlich die verschiedenen vollständigen Tutorials mit schrittweiser Hinzufügung und interaktiver Anzeige des angezeigten Ergebnisses. Es gibt auch Links zu Beispielprojekten. Mit diesen Tutorials können Sie schnell in die deklarative Welt von SwiftUI eintauchen.

Es ist auch erwähnenswert, dass SwiftUI noch nicht für Produktionsprojekte bereit ist, zu jung ist und mehr als ein Jahr vergeht, bevor es auf diese Weise verwendet werden kann. Vergessen Sie auch nicht, dass nur iOS 13.0+ Versionen unterstützt werden. Es ist aber auch erwähnenswert, dass SwiftUI auf allen Apple-Plattformen funktioniert, was ein großer Vorteil gegenüber UIKit ist!

Beginnen wir die Implementierung vom Hauptbildschirm unserer Anwendung aus. Gehen Sie zur Datei ContentView.swift und ändern Sie den aktuellen Code in diesen.

struct ContentView: View {
    @EnvironmentObject var store: Store
    @State private var isAddingMode: Bool = false
    
    var body: some View {
        NavigationView {
            WorkoutListView()
                .navigationBarTitle("Workouts diary", displayMode: .inline)
                .navigationBarItems(
                    leading: AddButton(isAddingMode: self.$isAddingMode),
                    trailing: TrailingView()
                )
        }
        .sheet(isPresented: $isAddingMode) {
            AddWorkoutView(isAddingMode: self.$isAddingMode)
                .environmentObject(self.store)
        }
    }
}

Die Inhaltsansicht ist eine Standardansicht in SwiftUI. Der aus meiner Sicht wichtigste Teil ist die Codezeile, die die Speichervariable enthält. Wir werden @EnvironmentObject erstellen. Auf diese Weise können wir Daten aus dem Store verwenden, wo immer dies erforderlich ist, und unsere Ansichten werden automatisch aktualisiert, wenn die Daten geändert werden. Dies ist so etwas wie Singleton für unseren Shop.

@EnvironmentObject var store: Store

Beachten Sie auch die folgende Codezeile:

@State private var isAddingMode: Bool = false

ZustandIst ein Wrapper, mit dem wir den Status einer Ansicht anzeigen können. SwiftUI speichert es in einem speziellen internen Speicher außerhalb der View-Struktur. Nur eine verknüpfte Ansicht kann darauf zugreifen. Einmal der EigenschaftswertZustand Änderungen, SwiftUI erstellt die Ansicht neu, um Statusänderungen zu berücksichtigen.

Bild

Dann gehen wir zur Datei SceneDelegate.swift und fügen der Methode den Code hinzu:

func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        let contentView = ContentView().environmentObject(Store())
        
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

Auf die gleiche Weise kann jedes @EnvironmentObject an jede untergeordnete Darstellung in der gesamten Anwendung übergeben werden, und all dies ist dank der Umgebung möglich. Die Variable IsAddingMode ist markiertZustandund gibt an, ob die sekundäre Ansicht angezeigt wird oder nicht. Die Speichervariable wird automatisch von WorkoutListView geerbt , und wir müssen sie nicht explizit übergeben, aber wir müssen dies für AddWorkoutView tun , da sie in Form eines Blattes dargestellt wird, das kein untergeordnetes Element von ContentView ist .

Erstellen Sie nun eine WorkoutListView , die von View erbt. Erstellen Sie eine neue schnelle Datei mit dem Namen WorkoutListView .

struct WorkoutListView: View {
    @EnvironmentObject var store: Store
    
    var body: some View {
        List {
            ForEach(store.state.workouts) {
                WorkoutView(workout: $0)
            }
            .onDelete {
                self.store.dispatch(action: .removeWorkout(at: $0))
            }
            .listRowInsets(EdgeInsets())
        }
    }
}

Ansicht, die das Containerlistenelement verwendet, um eine Liste der Trainingseinheiten anzuzeigen. Die Funktion onDelete wird zum Löschen eines Trainings verwendet und verwendet die Aktion removeWorkout , die mit der vom Store bereitgestellten Versandfunktion ausgeführt wird . Um das Training in der Liste anzuzeigen, wird WorkoutView verwendet. Erstellen Sie eine weitere Datei WorkoutView.swift, die für die Anzeige unseres Artikels in der Liste verantwortlich ist.



struct WorkoutView: View {
    let workout: Workout
    
    private var backgroundColor: Color {
        switch workout.complexity {
        case .low:
            return .green
        case .medium:
            return .orange
        case .high:
            return .red
        }
    }
    
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(workout.name)
                Text("Distance:" + workout.distance + "km")
                    .font(.subheadline)
            }
            Spacer()
            VStack(alignment: .leading) {
                Text(simpleFormat(workout.date))
            }
        }
        .padding()
        .background(backgroundColor)
    }
}

private extension WorkoutView {
    func simpleFormat(_ date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "dd MMM yyyy"
        dateFormatter.locale = .init(identifier: "en_GB")
        return dateFormatter.string(from: date)
    }
}

Diese Ansicht verwendet das Trainingsobjekt als Parameter und wird basierend auf seinen Eigenschaften konfiguriert.

Um der Liste ein neues Element hinzuzufügen, müssen Sie den Parameter isAddingMode in true ändern , um AddWorkoutView anzuzeigen . Diese Verantwortung liegt bei AddButton .

struct AddButton: View {
    @Binding var isAddingMode: Bool
    
    var body: some View {
        Button(action: { self.isAddingMode = true }) {
            Image(systemName: "plus")
        }
    }
}

AddButton ist es auch wert, in eine separate Datei eingefügt zu werden.

Diese Ansicht ist eine einfache Schaltfläche, die zur besseren Strukturierung und Codetrennung aus der Hauptinhaltsansicht extrahiert wurde .

Erstellen Sie eine Ansicht, um ein neues Training hinzuzufügen. Erstellen Sie eine neue AddWorkoutView.swift- Datei :

struct AddWorkoutView: View {
    @EnvironmentObject private var store: Store
    
    @State private var nameText: String = ""
    @State private var distanceText: String = ""
    @State private var complexityField: Complexity = .medium
    @State private var dateField: Date = Date()
    @Binding var isAddingMode: Bool
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Name", text: $nameText)
                TextField("Distance", text: $distanceText)
                Picker(selection: $complexityField, label: Text("Complexity")) {
                    Text("Low").tag(Complexity.low)
                    Text("Medium").tag(Complexity.medium)
                    Text("High").tag(Complexity.high)
                }
                DatePicker(selection: $dateField, displayedComponents: .date) {
                    Text("Date")
                }
            }
            .navigationBarTitle("Workout Details", displayMode: .inline)
            .navigationBarItems(
                leading: Button(action: { self.isAddingMode = false }) {
                    Text("Cancel")
                },
                trailing: Button(action: {
                    let workout = Workout(
                        name: self.nameText,
                        distance: self.distanceText,
                        date: self.dateField,
                        complexity: self.complexityField
                    )
                    self.store.dispatch(action: .addWorkout(workout))
                    self.isAddingMode = false
                }) {
                    Text("Save")
                }
                .disabled(nameText.isEmpty)
            )
        }
    }
}

Dies ist ein ziemlich großer Controller, der wie andere Controller die Speichervariable enthält. Es enthält auch die Variablen nameText, distanceText, complexificationField und isAddingMode . Die ersten drei Variablen sind für die Verknüpfung von TextField, Picker und DatePicker erforderlich , die auf diesem Bildschirm angezeigt werden. Die Navigationsleiste besteht aus zwei Elementen. Die erste Schaltfläche schließt den Bildschirm, ohne ein neues Training hinzuzufügen, und die letzte Schaltfläche fügt der Liste ein neues Training hinzu. Dies wird durch Senden der Aktion addWorkout erreicht. Diese Aktion schließt auch den Bildschirm zum Hinzufügen eines neuen Trainings.

Bild

Last but not least ist TrailingView .

struct TrailingView: View {
    @EnvironmentObject var store: Store
    
    var body: some View {
        HStack(alignment: .center, spacing: 30) {
            Button(action: {
                switch self.store.state.sortType {
                case .distance:
                    self.store.dispatch(action: .sort(by: .distance))
                default:
                    self.store.dispatch(action: .sort(by: .complexity))
                }
            }) {
                Image(systemName: "arrow.up.arrow.down")
            }
            EditButton()
        }
    }
}

Diese Ansicht besteht aus zwei Schaltflächen, die zum Sortieren der Trainingsliste und zum Aktivieren oder Deaktivieren des Bearbeitungsmodus unserer Trainingsliste verantwortlich sind. Sortieraktionen werden mit der Versandfunktion aufgerufen, die wir über die Store-Eigenschaft aufrufen können.

Bild

Ergebnis


Die Anwendung ist fertig und sollte genau wie erwartet funktionieren. Versuchen wir es zu kompilieren und auszuführen.

Ergebnisse


Redux und SwiftUI arbeiten sehr gut zusammen. Mit diesen Tools geschriebener Code ist leicht zu verstehen und kann gut organisiert werden. Ein weiterer guter Aspekt dieser Lösung ist die hervorragende Codetestbarkeit. Diese Lösung ist jedoch nicht ohne Nachteile. Eine davon ist eine große Menge an Speicher, die von der Anwendung verwendet wird, wenn der Status der Anwendung sehr komplex ist, und die Anwendungsleistung in bestimmten Szenarien möglicherweise nicht optimal ist, da alle Ansichten in SwiftUI beim Erstellen eines neuen Status aktualisiert werden. Diese Mängel können einen großen Einfluss auf die Qualität der Anwendung und die Benutzerinteraktion haben. Wenn wir uns jedoch an sie erinnern und den Status auf angemessene Weise vorbereiten, können die negativen Auswirkungen leicht minimiert oder sogar vermieden werden.

Sie können den Quellcode des Projekts herunterladen oder anzeigen.hier .

Ich hoffe, dass Ihnen dieser Artikel gefallen hat und Sie etwas Neues für sich gelernt haben. Bis bald. Weiter wird es noch interessanter.

All Articles