Redux sind wie State Container in SwiftUI. Die Grundlagen

Bild

Diese Woche werden wir über das Erstellen eines Statuscontainers sprechen, der dem von Redux verwendeten ähnelt. Es ist die einzige Wertquelle für die in der Entwicklung befindliche Anwendung. Ein einziger Status für die gesamte Anwendung erleichtert das Debuggen und Überprüfen. Eine einzige Quelle für Wahrheitswerte beseitigt Tausende von Fehlern, die beim Erstellen mehrerer Status in einer Anwendung auftreten.

Eine einzige Quelle für Wahrheitswerte


Die Hauptidee besteht darin, den Zustand der gesamten Anwendung durch eine einzige Struktur oder Zusammensetzung von Strukturen zu beschreiben. Angenommen, wir arbeiten an einer Github-Repository-Suchanwendung, bei der state ein Array von Repositorys ist, die wir anhand einer bestimmten Anforderung mithilfe der Github-API auswählen.

struct AppState { var searchResult: [Repo] = [] } 

Der nächste Schritt besteht darin, den Status (schreibgeschützt) an jede Ansicht in der Anwendung zu übergeben. Der beste Weg, dies zu erreichen, ist die Verwendung der SwiftUI-Umgebung. Sie können auch ein Objekt, das den Status der gesamten Anwendung enthält, an die Umgebung der Basisansicht übergeben. Die Basisansicht teilt die Umgebung mit allen untergeordneten Ansichten. Weitere Informationen zur Umgebung von SwiftUI finden Sie in der Publikation Power of the Environment in SwiftUI .

 final class Store: ObservableObject { @Published private(set) var state: AppState } 

Im obigen Beispiel haben wir ein Geschäftsobjekt erstellt , das den Status der Anwendung speichert und schreibgeschützten Zugriff darauf bietet. Die State-Eigenschaft verwendet den Wrapper der @Published- Eigenschaft, der SwiftUI über alle Änderungen benachrichtigt. Damit können Sie die gesamte Anwendung ständig aktualisieren und aus einer einzigen Quelle von Wahrheitswerten ableiten. Früher haben wir in früheren Artikeln über Speicherobjekte gesprochen. Um mehr darüber zu erfahren, müssen Sie den Artikel „ Modellieren des Anwendungsstatus mithilfe von Speicherobjekten in SwiftUIlesen .

Reduzierer und Aktionen


Es ist Zeit, über Benutzeraktionen zu sprechen, die zu Statusänderungen führen. Eine Aktion ist eine einfache Aufzählung oder Sammlung von Aufzählungen, die eine Statusänderung beschreiben. Legen Sie beispielsweise den Ladewert während der Datenerfassung fest, weisen Sie die resultierenden Repositorys einer Statuseigenschaft zu, usw. Betrachten Sie nun den Beispielcode für eine Aufzählung von Action.

 enum AppAction { case search(query: String) case setSearchResult(repos: [Repo]) } 

Reduzieren ist eine Funktion, die den aktuellen Status übernimmt, eine Aktion auf den Status anwendet und einen neuen Status generiert. Normalerweise ist das Reduzierstück oder die Zusammensetzung der Reduzierstücke der einzige Ort in einer Anwendung, an dem sich der Zustand ändert. Die Tatsache, dass eine einzelne Funktion den gesamten Status einer Anwendung ändern kann, macht den Code sehr einfach, leicht zu testen und leicht zu debuggen. Das Folgende ist ein Beispiel für eine Reduktionsfunktion.

 struct Reducer<State, Action> { let reduce: (inout State, Action) -> Void } let appReducer: Reducer<AppState, AppAction> = Reducer { state, action in switch action { case let .setSearchResults(repos): state.searchResult = repos } } 

Unidirektionale Strömung


Jetzt ist es Zeit, über den Datenfluss zu sprechen. Jede Ansicht hat nur Lesezugriff auf den Status über das Geschäftsobjekt. Ansichten können Aktionen an das Repository-Objekt senden. Der Reduzierer ändert den Status und SwiftUI benachrichtigt dann alle Ansichten über Statusänderungen. SwiftUI verfügt über einen äußerst effizienten Vergleichsalgorithmus, sodass der Status der gesamten Anwendung angezeigt und geänderte Ansichten sehr schnell aktualisiert werden können.

Zustand -> Ansicht -> Aktion -> Zustand -> Ansicht

Diese Architektur arbeitet nur mit einem unidirektionalen Datenstrom. Dies bedeutet, dass alle Daten in der Anwendung dem gleichen Muster folgen, wodurch die Logik der erstellten Anwendung vorhersehbarer und verständlicher wird. Lassen Sie uns das Geschäftsobjekt ändern, um das Senden von Aktionen zu unterstützen.

 final class Store<State, Action>: ObservableObject { @Published private(set) var state: State private let appReducer: Reducer<State, Action> init(initialState: State, appReducer: @escaping Reducer<State, Action>) { self.state = initialState self.appReducer = appReducer } func send(_ action: Action) { appReducer.reduce(&state, action) } } 

Nebenwirkungen


Wir haben bereits einen unidirektionalen Stream implementiert, der Benutzeraktionen akzeptiert und den Status ändert, aber was ist mit einer asynchronen Aktion, die wir normalerweise als Nebeneffekte bezeichnen . Wie füge ich Unterstützung für asynchrone Tasks für den verwendeten Speichertyp hinzu? Ich denke, es ist Zeit, die Verwendung des Combine Framework einzuführen, das sich ideal für die Bearbeitung asynchroner Aufgaben eignet.

 import Foundation import Combine protocol Effect { associatedtype Action func mapToAction() -> AnyPublisher<Action, Never> } enum SideEffect: Effect { case search(query: String) func mapToAction() -> AnyPublisher<Action, Never> { switch self { case let .search(query): return dependencies.githubService .searchPublisher(matching: query) .replaceError(with: []) .map { AppAction.setSearchResults(repos: $0) } .eraseToAnyPublisher() } } } 

Durch die Einführung des Effect-Protokolls wurde die Unterstützung für asynchrone Aufgaben hinzugefügt. Effect ist eine Actions-Sequenz, die mit dem Publisher- Typ aus dem Combine-Framework veröffentlicht werden kann . Auf diese Weise können Sie asynchrone Jobs mit der Option "Kombinieren" verarbeiten und anschließend Aktionen veröffentlichen, mit denen der Reduzierer Aktionen auf den aktuellen Status anwendet.

 final class Store<State, Action>: ObservableObject { @Published private(set) var state: State private let appReducer: Reducer<State, Action> private var cancellables: Set<AnyCancellable> = [] init(initialState: State, appReducer: Reducer<State, Action>) { self.state = initialState self.appReducer = appReducer } func send(_ action: Action) { appReducer.reduce(&state, action) } func send<E: Effect>(_ effect: E) where E.Action == Action { effect .mapToAction() .receive(on: DispatchQueue.main) .sink(receiveValue: send) .store(in: &cancellables) } } 

Praktisches Beispiel


Schließlich können wir die Repository- Suchanwendung vervollständigen, die die Github-API asynchron aufruft und die Repositorys auswählt, die der Abfrage entsprechen. Der vollständige Quellcode der Anwendung ist auf Github verfügbar.

 struct SearchContainerView: View { @EnvironmentObject var store: Store<AppState, AppAction> @State private var query: String = "Swift" var body: some View { SearchView( query: $query, repos: store.state.searchResult, onCommit: fetch ).onAppear(perform: fetch) } private func fetch() { store.send(SideEffect.search(query: query)) } } struct SearchView : View { @Binding var query: String let repos: [Repo] let onCommit: () -> Void var body: some View { NavigationView { List { TextField("Type something", text: $query, onCommit: onCommit) if repos.isEmpty { Text("Loading...") } else { ForEach(repos) { repo in RepoRow(repo: repo) } } }.navigationBarTitle(Text("Search")) } } } 

Teilen Sie den Bildschirm in zwei Ansichten auf: Containeransicht und Renderansicht . Die Containeransicht steuert die Aktionen und wählt die erforderlichen Teile aus dem globalen Status aus . Die Rendering-Ansicht empfängt Daten und zeigt sie an. Wir haben bereits in früheren Artikeln über Containeransichten gesprochen. Weitere Informationen finden Sie unter dem Link " Einführung in Containeransichten in SwiftUI ".

Schlussfolgerungen


Heute haben wir gelernt, wie man einen Redux-ähnlichen Zustandscontainer mit Blick auf Nebenwirkungen erstellt . Dazu haben wir die SwiftUI Environment-Funktion und das Combine Framework verwendet. Ich hoffe dieser Artikel war hilfreich.

Vielen Dank fürs Lesen und bis bald!

Source: https://habr.com/ru/post/de476398/


All Articles