L'architecture Composable (TCA, en abrégé) est une bibliothèque permettant de créer des applications de manière cohérente et compréhensible, en gardant à l'esprit la composition, les tests et l'ergonomie. Il peut être utilisé dans SwiftUI, UIKit, etc., et sur n'importe quelle plate-forme Apple (iOS, macOS, visionOS, tvOS et watchOS).
Qu'est-ce que l'architecture composable ?
Apprendre encore plus
Exemples
Utilisation de base
Documentation
Communauté
Installation
Traductions
Cette bibliothèque fournit quelques outils de base qui peuvent être utilisés pour créer des applications ayant des objectifs et une complexité variables. Il fournit des histoires convaincantes que vous pouvez suivre pour résoudre de nombreux problèmes que vous rencontrez quotidiennement lors de la création d'applications, tels que :
Gestion de l'État
Comment gérer l'état de votre application à l'aide de types de valeurs simples et partager l'état sur plusieurs écrans afin que les mutations sur un écran puissent être immédiatement observées sur un autre écran.
Composition
Comment décomposer de grandes fonctionnalités en composants plus petits qui peuvent être extraits dans leurs propres modules isolés et être facilement recollés pour former la fonctionnalité.
Effets secondaires
Comment permettre à certaines parties de l'application de communiquer avec le monde extérieur de la manière la plus testable et la plus compréhensible possible.
Essai
Comment non seulement tester une fonctionnalité intégrée à l'architecture, mais également écrire des tests d'intégration pour des fonctionnalités composées de nombreuses parties et écrire des tests de bout en bout pour comprendre comment les effets secondaires influencent votre application. Cela vous permet de garantir solidement que votre logique métier fonctionne comme prévu.
Ergonomie
Comment réaliser tout ce qui précède dans une API simple avec le moins de concepts et de pièces mobiles possible.
L'architecture Composable a été conçue au fil de nombreux épisodes sur Point-Free, une série de vidéos explorant la programmation fonctionnelle et le langage Swift, animée par Brandon Williams et Stephen Celis.
Vous pouvez regarder tous les épisodes ici, ainsi qu'une visite dédiée en plusieurs parties de l'architecture à partir de zéro.
Ce référentiel est livré avec de nombreux exemples pour montrer comment résoudre des problèmes courants et complexes avec l'architecture Composable. Consultez ce répertoire pour les voir tous, notamment :
Études de cas
Commencer
Effets
Navigation
Réducteurs d'ordre supérieur
Composants réutilisables
Gestionnaire de localisation
Gestionnaire de mouvement
Recherche
Reconnaissance vocale
Application SyncUps
Tic-Tac-Toe
Toutes les tâches
Mémos vocaux
Vous cherchez quelque chose de plus substantiel ? Découvrez le code source d'isowords, un jeu de recherche de mots iOS intégré à SwiftUI et à l'architecture Composable.
Note
Pour un didacticiel interactif étape par étape, assurez-vous de consulter Meet the Composable Architecture.
Pour créer une fonctionnalité à l'aide de l'architecture Composable, vous définissez certains types et valeurs qui modélisent votre domaine :
State : Un type qui décrit les données dont votre fonctionnalité a besoin pour exécuter sa logique et restituer son interface utilisateur.
Action : un type qui représente toutes les actions pouvant se produire dans votre fonctionnalité, telles que les actions des utilisateurs, les notifications, les sources d'événements, etc.
Réducteur : Une fonction qui décrit comment faire évoluer l'état actuel de l'application vers l'état suivant en fonction d'une action. Le réducteur est également chargé de renvoyer tous les effets qui doivent être exécutés, tels que les requêtes API, ce qui peut être effectué en renvoyant une valeur Effect
.
Store : le runtime qui pilote réellement votre fonctionnalité. Vous envoyez toutes les actions de l'utilisateur au magasin afin que celui-ci puisse exécuter le réducteur et les effets, et vous pouvez observer les changements d'état dans le magasin afin de pouvoir mettre à jour l'interface utilisateur.
Les avantages de cette procédure sont que vous débloquerez instantanément la testabilité de votre fonctionnalité et que vous pourrez diviser des fonctionnalités volumineuses et complexes en domaines plus petits qui peuvent être regroupés.
À titre d'exemple de base, considérons une interface utilisateur qui affiche un nombre ainsi que des boutons « + » et « - » qui incrémentent et décrémentent le nombre. Pour rendre les choses intéressantes, supposons qu'il existe également un bouton qui, lorsqu'il est enfoncé, effectue une requête API pour récupérer un fait aléatoire sur ce numéro et l'affiche dans la vue.
Pour implémenter cette fonctionnalité, nous créons un nouveau type qui hébergera le domaine et le comportement de la fonctionnalité, et il sera annoté avec la macro @Reducer
:
importer la fonctionnalité ComposableArchitecture@Reducerstruct {}
Ici, nous devons définir un type pour l'état de la fonctionnalité, qui consiste en un entier pour le décompte actuel, ainsi qu'une chaîne facultative qui représente le fait présenté :
@Reducerstruct Feature { @ObservableState struct State : Équatable { var count = 0 var numberFact : String ? }}
Note
Nous avons appliqué la macro @ObservableState
à State
afin de profiter des outils d'observation de la bibliothèque.
Nous devons également définir un type pour les actions de la fonctionnalité. Il existe des actions évidentes, comme appuyer sur le bouton de décrémentation, le bouton d'incrémentation ou le bouton de faits. Mais il y en a aussi quelques-uns qui ne sont pas évidents, comme l'action qui se produit lorsque nous recevons une réponse à la requête API de fait :
@Reducerstruct Feature { @ObservableState struct State : Equatable { /* ... */ } enum Action { case decrementButtonTapped case incrémentButtonTapped case numberFactButtonTapped case numberFactResponse(String) }}
Ensuite, nous implémentons la propriété body
, qui est responsable de la composition de la logique et du comportement réels de la fonctionnalité. Dans celui-ci, nous pouvons utiliser le réducteur Reduce
pour décrire comment changer l'état actuel vers l'état suivant et quels effets doivent être exécutés. Certaines actions n'ont pas besoin d'exécuter d'effets, et elles peuvent renvoyer .none
pour représenter cela :
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { /* ... */ } var body: some Réducteur{ Réduire { état, action dans changer d'action { 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 wait URLSession.shared.data( à partir de : URL (chaîne : "http://numbersapi.com/(count)/trivia") ! ) wait send( .numberFactResponse(String(decoding: data, as: UTF8.self)) ) } case let .numberFactResponse(fact): state.numberFact = retour de fait .none } } }}
Et puis enfin, nous définissons la vue qui affiche la fonctionnalité. Il conserve un StoreOf
afin de pouvoir observer tous les changements d'état et effectuer un nouveau rendu, et nous pouvons envoyer toutes les actions de l'utilisateur au magasin afin que l'état change :
struct FeatureView : View { let store : StoreOfvar body : some View { Form { Section { Text("(store.count)") Button("Decrement") { store.send(.decrementButtonTapped) } Button( "Incrément") { store.send(.incrementButtonTapped) } } Section { Button("Number fact") { store.send(.numberFactButtonTapped) } } si let fact = store.numberFact { Texte (fait) } } }}
Il est également simple de faire sortir un contrôleur UIKit de ce magasin. Vous pouvez observer les changements d'état dans le magasin dans viewDidLoad
, puis remplir les composants de l'interface utilisateur avec les données du magasin. Le code est un peu plus long que la version SwiftUI, nous l'avons donc réduit ici :
class FeatureViewController : UIViewController { let store : StoreOfinit(store : StoreOf ) { self.store = store super.init(nibName : nil, bundle : nil) } init requis ? (codeur : NSCoder) { fatalError("init(coder:) n'a pas été implémenté") } override func viewDidLoad() { super.viewDidLoad() laissez countLabel = UILabel() laissez decrementButton = UIButton() laissez incrémentButton = UIButton() laissez factLabel = UILabel() // Omis : ajouter des sous-vues et configurer des contraintes... observer { [moi faible] dans garde se laisser aller sinon {retour} countLabel.text = "(self.store.text)" factLabel.text = self.store.numberFact } } @objc private func incrémentButtonTapped() { self.store.send(.incrementButtonTapped) } @objc private func decrementButtonTapped() { self.store.send(.decrementButtonTapped) } @objc private func factButtonTapped() { self.store.send(.numberFactButtonTapped) }}
Une fois que nous sommes prêts à afficher cette vue, par exemple dans le point d'entrée de l'application, nous pouvons construire une boutique. Cela peut être fait en spécifiant l'état initial dans lequel démarrer l'application, ainsi que le réducteur qui alimentera l'application :
import ComposableArchitecture@mainstruct MyApp : App { var body : some Scene { WindowGroup { FeatureView ( magasin : Store(initialState : Feature.State()) { Feature() } ) } }}
Et cela suffit pour afficher quelque chose à l’écran avec lequel jouer. Cela représente certainement quelques étapes de plus que si vous deviez le faire à la manière de SwiftUI vanille, mais il y a quelques avantages. Cela nous donne une manière cohérente d'appliquer des mutations d'état, au lieu d'une logique de dispersion dans certains objets observables et dans diverses fermetures d'actions des composants de l'interface utilisateur. Cela nous donne également une manière concise d’exprimer les effets secondaires. Et nous pouvons immédiatement tester cette logique, y compris ses effets, sans faire beaucoup de travail supplémentaire.
Note
Pour des informations plus détaillées sur les tests, consultez l’article dédié aux tests.
Pour tester, utilisez un TestStore
, qui peut être créé avec les mêmes informations que le Store
, mais il effectue un travail supplémentaire pour vous permettre d'affirmer comment votre fonctionnalité évolue au fur et à mesure que les actions sont envoyées :
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature() }}
Une fois le magasin de test créé, nous pouvons l'utiliser pour faire une assertion de l'ensemble du flux d'étapes d'un utilisateur. Chaque étape du processus dont nous avons besoin pour prouver que cet état a changé nos attentes. Par exemple, nous pouvons simuler le flux utilisateur consistant à appuyer sur les boutons d'incrémentation et de décrémentation :
// Teste qu'appuyer sur les boutons d'incrémentation/décrémentation modifie le nombre d'attente store.send(.incrementButtonTapped) { $0.count = 1}attendre store.send(.decrementButtonTapped) { $0.count = 0}
De plus, si une étape provoque l'exécution d'un effet, qui réinjecte des données dans le magasin, nous devons l'affirmer. Par exemple, si nous simulons l'utilisateur appuyant sur le bouton de fait, nous nous attendons à recevoir une réponse de fait avec le fait, ce qui entraîne ensuite le remplissage de l'état numberFact
:
attendre store.send(.numberFactButtonTaped)attendre store.receive(.numberFactResponse) { $0.numberFact = ???}
Cependant, comment savoir quel fait va nous être renvoyé ?
Actuellement, notre réducteur utilise un effet qui s'étend au monde réel pour atteindre un serveur API, ce qui signifie que nous n'avons aucun moyen de contrôler son comportement. Nous sommes tributaires de notre connectivité internet et de la disponibilité du serveur API pour écrire ce test.
Il serait préférable que cette dépendance soit transmise au réducteur afin que nous puissions utiliser une dépendance active lors de l'exécution de l'application sur un appareil, mais utiliser une dépendance simulée pour les tests. Nous pouvons le faire en ajoutant une propriété au réducteur Feature
:
@Reducerstruct Feature { let numberFact : (Int) lancements asynchrones -> String //...}
Ensuite, nous pouvons l'utiliser dans l'implémentation reduce
:
cas .numberFactButtonTapped : return .run { [count = state.count] envoyer let fact = essayer wait self.numberFact(count) wait send(.numberFactResponse(fact)) }
Et au point d'entrée de l'application, nous pouvons fournir une version de la dépendance qui interagit réellement avec le serveur API du monde réel :
@mainstruct MyApp : App { var body : some Scene { WindowGroup { FeatureView ( magasin : Store (état initial : Feature.State ()) { Feature ( numberFact : { nombre dans let (data, _) = essayez d'attendre URLSession.shared.data ( de : URL (chaîne : "http://numbersapi.com/(number)") ! ) return String (décodage : données, comme : UTF8.self) } ) } ) } }}
Mais dans les tests, nous pouvons utiliser une dépendance fictive qui renvoie immédiatement un fait déterministe et prévisible :
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature(numberFact: { "($0) est un bon nombre Brent" }) }}
Avec ce petit travail initial, nous pouvons terminer le test en simulant l'utilisateur appuyant sur le bouton de fait, puis en recevant la réponse de la dépendance pour présenter le fait :
attendre store.send(.numberFactButtonTaped)attendre store.receive(.numberFactResponse) { $0.numberFact = "0 est un bon nombre Brent"}
Nous pouvons également améliorer l’ergonomie d’utilisation de la dépendance numberFact
dans notre application. Au fil du temps, l'application peut évoluer vers de nombreuses fonctionnalités, et certaines de ces fonctionnalités peuvent également vouloir accéder à numberFact
, et le transmettre explicitement à travers toutes les couches peut devenir ennuyeux. Il existe un processus que vous pouvez suivre pour « enregistrer » les dépendances auprès de la bibliothèque, les rendant instantanément disponibles pour n'importe quelle couche de l'application.
Note
Pour des informations plus détaillées sur la gestion des dépendances, consultez l'article dédié aux dépendances.
Nous pouvons commencer par envelopper la fonctionnalité de fait numérique dans un nouveau type :
struct NumberFactClient { var fetch : (Int) lancements asynchrones -> String}
Et puis en enregistrant ce type auprès du système de gestion des dépendances en conformant le client au protocole DependencyKey
, qui vous oblige à spécifier la valeur réelle à utiliser lors de l'exécution de l'application dans des simulateurs ou des appareils :
extension NumberFactClient : DependencyKey { static let liveValue = Self ( récupérer : { nombre dans let (data, _) = essayer d'attendre URLSession.shared .data(from : URL(string : "http://numbersapi.com/(number)")! ) return String(décodage : données, comme : UTF8.self) } )}extension DependencyValues { var numberFact: NumberFactClient { get { self[NumberFactClient.self] } set { self[NumberFactClient.self] = nouvelleValue } }}
Avec ce petit travail initial effectué, vous pouvez instantanément commencer à utiliser la dépendance dans n'importe quelle fonctionnalité en utilisant le wrapper de propriété @Dependency
:
@Réducteur struct Feature {- let numberFact : (Int) lancements asynchrones -> String+ @Dependency(.numberFact) var numberFact …- essayez d'attendre self.numberFact(count)+ essayez d'attendre self.numberFact.fetch(count) }
Ce code fonctionne exactement comme avant, mais vous n'avez plus besoin de transmettre explicitement la dépendance lors de la construction du réducteur de fonctionnalité. Lors de l'exécution de l'application dans les aperçus, le simulateur ou sur un appareil, la dépendance en direct sera fournie au réducteur, et lors des tests, la dépendance de test sera fournie.
Cela signifie que le point d'entrée de l'application n'a plus besoin de construire de dépendances :
@mainstruct MyApp : App { var body : some Scene { WindowGroup { FeatureView ( magasin : Store(initialState : Feature.State()) { Feature() } ) } }}
Et le magasin de tests peut être construit sans spécifier de dépendances, mais vous pouvez toujours remplacer toute dépendance dont vous avez besoin pour les besoins du test :
let store = TestStore(initialState : Feature.State()) { Feature()} withDependencies : { $0.numberFact.fetch = { "($0) est un bon nombre Brent" }}// ...
C'est la base de la création et du test d'une fonctionnalité dans l'architecture Composable. Il y a beaucoup plus de choses à explorer, comme la composition, la modularité, l'adaptabilité et les effets complexes. Le répertoire Exemples contient de nombreux projets à explorer pour voir des utilisations plus avancées.
La documentation des versions et main
est disponible ici :
main
1.17.0 (guide de migration)
1.16.0 (guide de migration)
1.15.0 (guide de migration)
1.14.0 (guide de migration)
1.13.0 (guide de migration)
1.12.0 (guide de migration)
1.11.0 (guide de migration)
1.10.0 (guide de migration)
1.9.0 (guide de migration)
1.8.0 (guide de migration)
1.7.0 (guide de migration)
1.6.0 (guide de migration)
1.5.0 (guide de migration)
1.4.0 (guide de migration)
1.3.0
1.2.0
1.1.0
1.0.0
0.59.0
0.58.0
0.57.0
Il y a un certain nombre d'articles dans la documentation qui pourraient vous être utiles à mesure que vous vous familiariserez avec la bibliothèque :
Commencer
Dépendances
Essai
Navigation
État de partage
Performance
Concurrence
Reliures
Si vous souhaitez discuter de l'architecture Composable ou si vous avez une question sur la façon de l'utiliser pour résoudre un problème particulier, il existe un certain nombre d'endroits où vous pouvez discuter avec d'autres passionnés de Point-Free :
Pour les discussions longues, nous recommandons l'onglet discussions de ce dépôt.
Pour une discussion informelle, nous recommandons le slack Point-Free Community.
Vous pouvez ajouter ComposableArchitecture à un projet Xcode en l'ajoutant en tant que dépendance de package.
Dans le menu Fichier , sélectionnez Ajouter des dépendances de package...
Entrez "https://github.com/pointfreeco/swift-composable-architecture" dans le champ de texte URL du référentiel de packages.
Selon la structure de votre projet :
Si vous disposez d'une seule cible d'application qui a besoin d'accéder à la bibliothèque, ajoutez ComposableArchitecture directement à votre application.
Si vous souhaitez utiliser cette bibliothèque à partir de plusieurs cibles Xcode, ou mélanger des cibles Xcode et des cibles SPM, vous devez créer un framework partagé qui dépend de ComposableArchitecture , puis dépendre de ce framework dans toutes vos cibles. Pour un exemple de ceci, consultez l'application de démonstration Tic-Tac-Toe, qui divise de nombreuses fonctionnalités en modules et consomme la bibliothèque statique de cette manière à l'aide du package Swift tic-tac-toe .
L'architecture Composable est conçue dans un souci d'extensibilité, et il existe un certain nombre de bibliothèques prises en charge par la communauté disponibles pour améliorer vos applications :
Extras de l'architecture Composable : une bibliothèque complémentaire à l'architecture Composable.
TCAComposer : un framework de macros pour générer du code passe-partout dans l'architecture Composable.
TCACoordinators : le modèle de coordinateur dans l'architecture Composable.
Si vous souhaitez contribuer à une bibliothèque, veuillez ouvrir un PR avec un lien vers celui-ci !
Les traductions suivantes de ce README ont été fournies par des membres de la communauté :
arabe
Français
hindi
indonésien
italien
japonais
coréen
polonais
portugais
russe
Chinois simplifié
Espagnol
ukrainien
Si vous souhaitez contribuer à une traduction, veuillez ouvrir un PR avec un lien vers un Gist !
Nous avons un article dédié à toutes les questions et commentaires les plus fréquemment posés concernant la bibliothèque.
Les personnes suivantes ont donné leur avis sur la bibliothèque à ses débuts et ont contribué à faire de la bibliothèque ce qu'elle est aujourd'hui :
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 Sima, Jasdev Singh, Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares et tous les abonnés Point-Free ?.
Un merci spécial à Chris Liscio qui nous a aidé à résoudre de nombreuses bizarreries étranges de SwiftUI et à affiner l'API finale.
Et grâce à Shai Mishali et au projet CombineCommunity, dont nous avons tiré leur implémentation de Publishers.Create
, que nous utilisons dans Effect
pour aider à relier les API basées sur les délégués et les rappels, ce qui facilite grandement l'interface avec les frameworks tiers.
L'architecture Composable a été construite sur une base d'idées lancées par d'autres bibliothèques, en particulier Elm et Redux.
Il existe également de nombreuses bibliothèques d'architecture dans la communauté Swift et iOS. Chacun d’eux a son propre ensemble de priorités et de compromis qui diffèrent de l’architecture Composable.
Côtes
Boucle
ReSwift
Flux de travail
Kit de réacteur
RxCommentaires
Mobius.swift
Fluxeur
Kit d'architecture promis
Cette bibliothèque est publiée sous licence MIT. Voir LICENCE pour plus de détails.