Die Composable Architecture (kurz TCA) ist eine Bibliothek zum konsistenten und verständlichen Erstellen von Anwendungen unter Berücksichtigung von Komposition, Tests und Ergonomie. Es kann in SwiftUI, UIKit und mehr sowie auf jeder Apple-Plattform (iOS, macOS, visionOS, tvOS und watchOS) verwendet werden.
Was ist die Composable Architecture?
Erfahren Sie mehr
Beispiele
Grundlegende Verwendung
Dokumentation
Gemeinschaft
Installation
Übersetzungen
Diese Bibliothek stellt einige Kernwerkzeuge bereit, die zum Erstellen von Anwendungen unterschiedlichen Zwecks und unterschiedlicher Komplexität verwendet werden können. Es bietet fesselnde Geschichten, denen Sie folgen können, um viele Probleme zu lösen, auf die Sie täglich beim Erstellen von Anwendungen stoßen, wie zum Beispiel:
Staatsverwaltung
So verwalten Sie den Status Ihrer Anwendung mithilfe einfacher Werttypen und teilen den Status über mehrere Bildschirme hinweg, sodass Änderungen auf einem Bildschirm sofort auf einem anderen Bildschirm beobachtet werden können.
Zusammensetzung
Wie man große Features in kleinere Komponenten zerlegt, die in ihre eigenen, isolierten Module extrahiert und einfach wieder zusammengeklebt werden können, um das Feature zu bilden.
Nebenwirkungen
Wie man bestimmte Teile der Anwendung auf möglichst testbare und verständliche Weise mit der Außenwelt kommunizieren lässt.
Testen
So testen Sie nicht nur eine in der Architektur integrierte Funktion, sondern schreiben auch Integrationstests für Funktionen, die aus vielen Teilen bestehen, und schreiben End-to-End-Tests, um zu verstehen, wie Nebenwirkungen Ihre Anwendung beeinflussen. Dadurch können Sie stark garantieren, dass Ihre Geschäftslogik wie erwartet funktioniert.
Ergonomie
So erreichen Sie all das in einer einfachen API mit möglichst wenigen Konzepten und beweglichen Teilen.
Die Composable Architecture wurde im Laufe vieler Episoden von Point-Free entworfen, einer Videoserie über funktionale Programmierung und die Swift-Sprache, moderiert von Brandon Williams und Stephen Celis.
Hier können Sie sich alle Episoden sowie einen speziellen, mehrteiligen Rundgang durch die Architektur von Grund auf ansehen.
Dieses Repo enthält viele Beispiele, die zeigen, wie man häufige und komplexe Probleme mit der Composable Architecture löst. Schauen Sie sich dieses Verzeichnis an, um sie alle zu sehen, einschließlich:
Fallstudien
Erste Schritte
Effekte
Navigation
Reduzierer höherer Ordnung
Wiederverwendbare Komponenten
Standortmanager
Bewegungsmanager
Suchen
Spracherkennung
SyncUps-App
Tic-Tac-Toe
Todos
Sprachnotizen
Suchen Sie etwas Substanzielleres? Schauen Sie sich den Quellcode für isowords an, ein iOS-Wortsuchspiel, das auf SwiftUI und der Composable Architecture basiert.
Notiz
Eine interaktive Schritt-für-Schritt-Anleitung finden Sie unter „Meet the Composable Architecture“.
Um ein Feature mithilfe der Composable Architecture zu erstellen, definieren Sie einige Typen und Werte, die Ihre Domäne modellieren:
Status : Ein Typ, der die Daten beschreibt, die Ihr Feature benötigt, um seine Logik auszuführen und seine Benutzeroberfläche darzustellen.
Aktion : Ein Typ, der alle Aktionen darstellt, die in Ihrer Funktion ausgeführt werden können, z. B. Benutzeraktionen, Benachrichtigungen, Ereignisquellen und mehr.
Reduzierer : Eine Funktion, die beschreibt, wie der aktuelle Status der App bei einer Aktion zum nächsten Status weiterentwickelt wird. Der Reduzierer ist auch dafür verantwortlich, alle Effekte zurückzugeben, die ausgeführt werden sollen, z. B. API-Anfragen, was durch die Rückgabe eines Effect
erfolgen kann.
Store : Die Laufzeit, die Ihre Funktion tatsächlich steuert. Sie senden alle Benutzeraktionen an den Store, damit der Store den Reduzierer und die Effekte ausführen kann, und Sie können Zustandsänderungen im Store beobachten, damit Sie die Benutzeroberfläche aktualisieren können.
Dies hat den Vorteil, dass Sie die Testbarkeit Ihrer Funktion sofort freischalten und große, komplexe Funktionen in kleinere Domänen aufteilen können, die zusammengefügt werden können.
Betrachten Sie als einfaches Beispiel eine Benutzeroberfläche, die eine Zahl zusammen mit den Schaltflächen „+“ und „−“ anzeigt, die die Zahl erhöhen und verringern. Um die Sache interessanter zu machen, nehmen wir an, dass es auch eine Schaltfläche gibt, die beim Antippen eine API-Anfrage sendet, um eine zufällige Tatsache über diese Zahl abzurufen und sie in der Ansicht anzuzeigen.
Um diese Funktion zu implementieren, erstellen wir einen neuen Typ, der die Domäne und das Verhalten der Funktion enthält, und er wird mit dem @Reducer
-Makro annotiert:
ComposableArchitecture@Reducerstruct-Feature importieren {}
Hier müssen wir einen Typ für den Status des Features definieren, der aus einer Ganzzahl für die aktuelle Anzahl sowie einer optionalen Zeichenfolge besteht, die die dargestellte Tatsache darstellt:
@Reducerstruct Feature { @ObservableState struct State: Equatable { var count = 0 var numberFact: String? }}
Notiz
Wir haben das @ObservableState
Makro auf State
angewendet, um die Beobachtungstools in der Bibliothek zu nutzen.
Wir müssen auch einen Typ für die Aktionen der Funktion definieren. Es gibt die offensichtlichen Aktionen, wie z. B. das Tippen auf die Schaltfläche „Verringern“, „Erhöhen“ oder „Fakten“. Aber es gibt auch einige etwas nicht offensichtliche, wie zum Beispiel die Aktion, die ausgeführt wird, wenn wir eine Antwort auf die Fakt-API-Anfrage erhalten:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { case decrementButtonTapped case incrementButtonTapped case numberFactButtonTapped case numberFactResponse(String) }}
Und dann implementieren wir die body
Eigenschaft, die für die Erstellung der eigentlichen Logik und des Verhaltens der Funktion verantwortlich ist. Darin können wir den Reduzierer Reduce
verwenden, um zu beschreiben, wie der aktuelle Zustand in den nächsten Zustand geändert wird und welche Effekte ausgeführt werden müssen. Einige Aktionen müssen keine Effekte ausführen und können .none
zurückgeben, um Folgendes darzustellen:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { /* ... */ } var body: some Reducer{ Reduce { state, action in Aktion wechseln { case .decrementButtonTapped: state.count -= 1 return .none case .incrementButtonTapped: state.count += 1 return .none case .numberFactButtonTapped: return .run { [count = state.count] send in let (data, _) = try waiting URLSession.shared.data( von: URL(string: "http://numbersapi.com/(count)/trivia")! ) wait send( .numberFactResponse(String(decoding: data, as: UTF8.self)) ) } case let .numberFactResponse(fact): state.numberFact = Faktenrückgabe .none } } }}
Und schließlich definieren wir die Ansicht, die das Feature anzeigt. Es behält ein StoreOf
bei, damit es alle Änderungen am Status beobachten und erneut rendern kann, und wir können alle Benutzeraktionen an den Store senden, damit sich der Status ändert:
struct FeatureView: View { let store: StoreOfvar body: some View { Form { Section { Text("(store.count)") Button("Decrement") { store.send(.decrementButtonTapped) } Button( "Inkrement") { store.send(.incrementButtonTapped) } } Abschnitt { Button("Number fact") { store.send(.numberFactButtonTapped) } } if let fact = store.numberFact { Text(fact) } } }}
Es ist auch einfach, einen UIKit-Controller von diesem Store aus steuern zu lassen. Sie können Zustandsänderungen im Store in viewDidLoad
beobachten und dann die UI-Komponenten mit Daten aus dem Store füllen. Der Code ist etwas länger als die SwiftUI-Version, daher haben wir ihn hier reduziert:
Klasse FeatureViewController: UIViewController { let store: StoreOfinit(store: StoreOf ) { self.store = store super.init(nibName: nil, bundle: nil) } erforderlich init?(coder: NSCoder) { fatalError("init(coder:) wurde nicht implementiert") } func viewDidLoad() überschreiben super.viewDidLoad() let countLabel = UILabel() let decrementButton = UIButton() let incrementButton = UIButton() let factLabel = UILabel() // Ausgelassen: Unteransichten hinzufügen und Einschränkungen einrichten ... beobachten { [schwaches Selbst] in Wache, lass dich selbst else { return } countLabel.text = "(self.store.text)" factLabel.text = self.store.numberFact } } @objc private func incrementButtonTapped() { self.store.send(.incrementButtonTapped) } @objc private func decrementButtonTapped() { self.store.send(.decrementButtonTapped) } @objc private func factButtonTapped() { self.store.send(.numberFactButtonTapped) }}
Sobald wir bereit sind, diese Ansicht anzuzeigen, beispielsweise im Einstiegspunkt der App, können wir einen Store erstellen. Dies kann erreicht werden, indem der Anfangszustand angegeben wird, in dem die Anwendung gestartet wird, sowie der Reduzierer, der die Anwendung antreibt:
import ComposableArchitecture@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( store: Store(initialState: Feature.State()) { Feature() } ) } }}
Und das reicht aus, um etwas zum Spielen auf den Bildschirm zu bringen. Es sind definitiv ein paar Schritte mehr, als wenn Sie dies auf die übliche SwiftUI-Art tun würden, aber es gibt ein paar Vorteile. Es gibt uns eine konsistente Möglichkeit, Zustandsmutationen anzuwenden, anstatt die Logik in einigen beobachtbaren Objekten und in verschiedenen Aktionsabschlüssen von UI-Komponenten zu verstreuen. Es gibt uns auch eine prägnante Möglichkeit, Nebenwirkungen auszudrücken. Und wir können diese Logik inklusive der Effekte ohne großen Mehraufwand sofort testen.
Notiz
Ausführlichere Informationen zum Testen finden Sie im entsprechenden Testartikel.
Verwenden Sie zum Testen einen TestStore
, der mit den gleichen Informationen wie der Store
erstellt werden kann, aber zusätzliche Arbeit leistet, damit Sie feststellen können, wie sich Ihre Funktion entwickelt, wenn Aktionen gesendet werden:
@Testfunc-Grundlagen() async { let store = TestStore(initialState: Feature.State()) { Feature() }}
Sobald der Testspeicher erstellt ist, können wir ihn verwenden, um eine Aussage über den gesamten Benutzerschrittablauf zu machen. Bei jedem Schritt auf dem Weg, den wir beweisen müssen, hat sich dieser Zustand so verändert, wie wir es erwarten. Beispielsweise können wir den Benutzerablauf simulieren, der beim Tippen auf die Schaltflächen zum Erhöhen und Verringern entsteht:
// Testen Sie, ob das Tippen auf die Schaltflächen zum Erhöhen/Verringern den Countawait ändert. store.send(.incrementButtonTapped) { $0.count = 1}await store.send(.decrementButtonTapped) { $0.count = 0}
Wenn ein Schritt außerdem dazu führt, dass ein Effekt ausgeführt wird, der Daten zurück in den Speicher einspeist, müssen wir dies bestätigen. Wenn wir beispielsweise simulieren, dass der Benutzer auf die Fakt-Schaltfläche tippt, erwarten wir, dass wir eine Fakt-Antwort mit dem Fakt zurückerhalten, was dann dazu führt, dass der Status numberFact
gefüllt wird:
wait store.send(.numberFactButtonTapped)await store.receive(.numberFactResponse) { $0.numberFact = ???}
Doch woher wissen wir, welche Tatsache an uns zurückgesendet wird?
Derzeit nutzt unser Reduzierer einen Effekt, der bis in die reale Welt reicht, um einen API-Server zu treffen, und das bedeutet, dass wir sein Verhalten nicht kontrollieren können. Um diesen Test zu schreiben, sind wir auf die Launen unserer Internetverbindung und der Verfügbarkeit des API-Servers angewiesen.
Es wäre besser, diese Abhängigkeit an den Reduzierer zu übergeben, damit wir beim Ausführen der Anwendung auf einem Gerät eine Live-Abhängigkeit verwenden können, für Tests jedoch eine simulierte Abhängigkeit. Wir können dies tun, indem wir dem Feature
Reduzierer eine Eigenschaft hinzufügen:
@Reducerstruct Feature { let numberFact: (Int) async throws -> String // ...}
Dann können wir es in der reduce
-Implementierung verwenden:
case .numberFactButtonTapped: return .run { [count = state.count] einsenden let fact = versuchen, auf self.numberFact(count) warten, auf send(.numberFactResponse(fact)) }
Und am Einstiegspunkt der Anwendung können wir eine Version der Abhängigkeit bereitstellen, die tatsächlich mit dem realen API-Server interagiert:
@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( speichern: Store(initialState: Feature.State()) { Feature( numberFact: { number in let (data, _) = try waiting URLSession.shared.data( von: URL(string: "http://numbersapi.com/(number)")! ) return String(decoding: data, as: UTF8.self) } ) } ) } }}
Aber in Tests können wir eine Scheinabhängigkeit verwenden, die sofort eine deterministische, vorhersehbare Tatsache zurückgibt:
@Testfunc Basics() async { let store = TestStore(initialState: Feature.State()) { Feature(numberFact: { "($0) is a good number Brent" }) }}
Mit dieser kleinen Vorarbeit können wir den Test abschließen, indem wir simulieren, dass der Benutzer auf die Faktenschaltfläche tippt und dann die Antwort von der Abhängigkeit erhält, um die Tatsache darzustellen:
wait store.send(.numberFactButtonTapped)await store.receive(.numberFactResponse) { $0.numberFact = „0 ist eine gute Zahl, Brent“}
Wir können auch die Ergonomie der Verwendung der numberFact
Abhängigkeit in unserer Anwendung verbessern. Im Laufe der Zeit kann sich die Anwendung zu vielen Funktionen weiterentwickeln, und einige dieser Funktionen benötigen möglicherweise auch Zugriff auf numberFact
, und die explizite Weiterleitung durch alle Ebenen kann lästig sein. Es gibt einen Prozess, den Sie befolgen können, um Abhängigkeiten in der Bibliothek zu „registrieren“, sodass sie sofort für jede Ebene in der Anwendung verfügbar sind.
Notiz
Ausführlichere Informationen zum Abhängigkeitsmanagement finden Sie im entsprechenden Artikel zu Abhängigkeiten.
Wir können damit beginnen, die Zahlenfaktenfunktion in einen neuen Typ zu packen:
struct NumberFactClient { var fetch: (Int) async throws -> String}
Und dann registrieren Sie diesen Typ beim Abhängigkeitsverwaltungssystem, indem Sie den Client an das DependencyKey
-Protokoll anpassen, das erfordert, dass Sie den Live-Wert angeben, der beim Ausführen der Anwendung in Simulatoren oder Geräten verwendet werden soll:
Erweiterung NumberFactClient: DependencyKey { static let liveValue = Self( fetch: { number in let (data, _) = try waiting URLSession.shared .data(from: URL(string: "http://numbersapi.com/(number)")! ) return String(decoding: data, as: UTF8.self) } )}extension DependencyValues { var numberFact: NumberFactClient { get { self[NumberFactClient.self] } set { self[NumberFactClient.self] = newValue } }}
Nachdem Sie ein wenig Vorarbeit geleistet haben, können Sie sofort damit beginnen, die Abhängigkeit in jeder Funktion zu nutzen, indem Sie den @Dependency
Eigenschaftswrapper verwenden:
@Reducer struct Feature {- let numberFact: (Int) async throws -> String+ @Dependency(.numberFact) var numberFact …- Versuchen Sie, auf self.numberFact(count) zu warten + Versuchen Sie, auf self.numberFact.fetch(count) zu warten. }
Dieser Code funktioniert genauso wie zuvor, aber Sie müssen die Abhängigkeit beim Erstellen des Reduzierers des Features nicht mehr explizit übergeben. Wenn die App in Vorschauen, im Simulator oder auf einem Gerät ausgeführt wird, wird die Live-Abhängigkeit dem Reduzierer bereitgestellt, und in Tests wird die Testabhängigkeit bereitgestellt.
Dies bedeutet, dass der Einstiegspunkt zur Anwendung keine Abhängigkeiten mehr erstellen muss:
@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( store: Store(initialState: Feature.State()) { Feature() } ) } }}
Und der Testspeicher kann ohne Angabe von Abhängigkeiten erstellt werden, Sie können aber dennoch alle Abhängigkeiten überschreiben, die Sie für den Testzweck benötigen:
let store = TestStore(initialState: Feature.State()) { Feature()} withDependencies: { $0.numberFact.fetch = { "($0) ist eine gute Zahl Brent" }}// ...
Das sind die Grundlagen zum Erstellen und Testen einer Funktion in der Composable Architecture. Es gibt noch viel mehr zu erforschen, etwa Komposition, Modularität, Anpassungsfähigkeit und komplexe Effekte. Das Beispielverzeichnis enthält eine Reihe von Projekten, die Sie erkunden können, um fortgeschrittenere Verwendungsmöglichkeiten kennenzulernen.
Die Dokumentation für Releases und main
ist hier verfügbar:
main
1.17.0 (Migrationsleitfaden)
1.16.0 (Migrationsleitfaden)
1.15.0 (Migrationsleitfaden)
1.14.0 (Migrationsleitfaden)
1.13.0 (Migrationsleitfaden)
1.12.0 (Migrationsleitfaden)
1.11.0 (Migrationsleitfaden)
1.10.0 (Migrationsleitfaden)
1.9.0 (Migrationsleitfaden)
1.8.0 (Migrationsleitfaden)
1.7.0 (Migrationsleitfaden)
1.6.0 (Migrationsleitfaden)
1.5.0 (Migrationsleitfaden)
1.4.0 (Migrationsleitfaden)
1.3.0
1.2.0
1.1.0
1.0.0
0,59,0
0,58,0
0,57,0
Die Dokumentation enthält eine Reihe von Artikeln, die Ihnen möglicherweise hilfreich sein können, wenn Sie sich mit der Bibliothek vertrauter machen:
Erste Schritte
Abhängigkeiten
Testen
Navigation
Freigabestatus
Leistung
Parallelität
Bindungen
Wenn Sie über die Composable Architecture diskutieren möchten oder eine Frage dazu haben, wie Sie sie zur Lösung eines bestimmten Problems verwenden können, gibt es eine Reihe von Orten, an denen Sie mit anderen Point-Free-Enthusiasten diskutieren können:
Für längere Diskussionen empfehlen wir die Registerkarte „Diskussionen“ dieses Repos.
Für ungezwungene Chats empfehlen wir den Point-Free Community-Slack.
Sie können ComposableArchitecture zu einem Xcode-Projekt hinzufügen, indem Sie es als Paketabhängigkeit hinzufügen.
Wählen Sie im Menü „Datei “ die Option „Paketabhängigkeiten hinzufügen...“ aus.
Geben Sie „https://github.com/pointfreeco/swift-composable-architecture“ in das Textfeld für die Paket-Repository-URL ein
Abhängig davon, wie Ihr Projekt strukturiert ist:
Wenn Sie ein einzelnes Anwendungsziel haben, das Zugriff auf die Bibliothek benötigt, fügen Sie ComposableArchitecture direkt zu Ihrer Anwendung hinzu.
Wenn Sie diese Bibliothek von mehreren Xcode-Zielen aus verwenden oder Xcode-Ziele und SPM-Ziele mischen möchten, müssen Sie ein gemeinsames Framework erstellen, das von ComposableArchitecture abhängt, und dann in allen Ihren Zielen von diesem Framework abhängig sein. Ein Beispiel hierfür finden Sie in der Tic-Tac-Toe-Demoanwendung, die viele Funktionen in Module aufteilt und die statische Bibliothek auf diese Weise mithilfe des Tic-Tac-Toe -Swift-Pakets nutzt.
Die Composable-Architektur ist auf Erweiterbarkeit ausgelegt und es stehen eine Reihe von Community-unterstützten Bibliotheken zur Verfügung, um Ihre Anwendungen zu erweitern:
Composable Architecture Extras: Eine Begleitbibliothek zur Composable Architecture.
TCAComposer: Ein Makro-Framework zum Generieren von Boiler-Plate-Code in der Composable Architecture.
TCACoordinators: Das Koordinatormuster in der Composable Architecture.
Wenn Sie eine Bibliothek beisteuern möchten, öffnen Sie bitte eine PR mit einem Link dazu!
Die folgenden Übersetzungen dieser README-Datei wurden von Mitgliedern der Community beigesteuert:
Arabisch
Französisch
Hindi
Indonesisch
Italienisch
japanisch
Koreanisch
Polieren
Portugiesisch
Russisch
Vereinfachtes Chinesisch
Spanisch
ukrainisch
Wenn Sie eine Übersetzung beisteuern möchten, öffnen Sie bitte eine PR mit einem Link zu einem Gist!
Wir haben einen eigenen Artikel für alle am häufigsten gestellten Fragen und Kommentare zur Bibliothek.
Die folgenden Personen gaben in ihrer Anfangsphase Feedback zur Bibliothek und trugen dazu bei, die Bibliothek zu dem zu machen, was sie heute ist:
Paul Colton, Kaan Dedeoglu, Matt Diephouse, Josef Doležal, Eimantas, Matthew Johnson, George Kaimakas, Nikita Leonov, Christopher Liscio, Jeffrey Macko, Alejandro Martinez, Shai Mishali, Willis Plummer, Simon-Pierre Roy, Justin Price, Sven A. Schmidt , Kyle Sherman, Petr Šíma, Jasdev Singh, Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares und alle Point-Free-Abonnenten?
Besonderer Dank geht an Chris Liscio, der uns geholfen hat, viele seltsame SwiftUI-Macken zu überwinden und die endgültige API zu verfeinern.
Und vielen Dank an Shai Mishali und das CombineCommunity-Projekt, von dem wir die Implementierung von Publishers.Create
übernommen haben, die wir in Effect
verwenden, um die Verbindung zwischen delegierten und rückrufbasierten APIs zu erleichtern und so die Schnittstelle zu Frameworks von Drittanbietern erheblich zu vereinfachen.
Die Composable Architecture basiert auf Ideen anderer Bibliotheken, insbesondere Elm und Redux.
Es gibt auch viele Architekturbibliotheken in der Swift- und iOS-Community. Jede davon hat ihre eigenen Prioritäten und Kompromisse, die sich von der Composable Architecture unterscheiden.
Rippen
Schleife
ReSwift
Arbeitsablauf
ReaktorKit
RxFeedback
Mobius.swift
Flussmittel
PromisedArchitectureKit
Diese Bibliothek wird unter der MIT-Lizenz veröffentlicht. Weitere Informationen finden Sie unter LIZENZ.