Composable Architecture (TCA, para abreviar) es una biblioteca para crear aplicaciones de una manera consistente y comprensible, teniendo en cuenta la composición, las pruebas y la ergonomía. Se puede utilizar en SwiftUI, UIKit y más, y en cualquier plataforma Apple (iOS, macOS, visionOS, tvOS y watchOS).
¿Qué es la Arquitectura Composable?
Más información
Ejemplos
Uso básico
Documentación
Comunidad
Instalación
Traducciones
Esta biblioteca proporciona algunas herramientas básicas que se pueden utilizar para crear aplicaciones de diversos propósitos y complejidad. Proporciona historias convincentes que puede seguir para resolver muchos de los problemas que encuentra a diario al crear aplicaciones, como por ejemplo:
Gestión estatal
Cómo administrar el estado de su aplicación utilizando tipos de valores simples y compartir el estado en muchas pantallas para que las mutaciones en una pantalla se puedan observar inmediatamente en otra pantalla.
Composición
Cómo dividir funciones grandes en componentes más pequeños que se pueden extraer en sus propios módulos aislados y volver a pegarlos fácilmente para formar la función.
Efectos secundarios
Cómo permitir que ciertas partes de la aplicación hablen con el mundo exterior de la manera más comprobable y comprensible posible.
Pruebas
Cómo no solo probar una característica integrada en la arquitectura, sino también escribir pruebas de integración para características que se han compuesto de muchas partes y escribir pruebas de un extremo a otro para comprender cómo los efectos secundarios influyen en su aplicación. Esto le permite ofrecer garantías sólidas de que su lógica empresarial se ejecuta de la manera esperada.
Ergonomía
Cómo lograr todo lo anterior en una API simple con la menor cantidad de conceptos y partes móviles posible.
La Arquitectura Composable se diseñó a lo largo de muchos episodios de Point-Free, una serie de videos que explora la programación funcional y el lenguaje Swift, presentada por Brandon Williams y Stephen Celis.
Puedes ver todos los episodios aquí, así como un recorrido dedicado en varias partes por la arquitectura desde cero.
Este repositorio viene con muchos ejemplos para demostrar cómo resolver problemas comunes y complejos con la Arquitectura Composable. Consulte este directorio para verlos todos, incluido:
Estudios de caso
Empezando
Efectos
Navegación
Reductores de orden superior
Componentes reutilizables
Gerente de ubicación
administrador de movimiento
Buscar
Reconocimiento de voz
Aplicación SyncUps
tres en raya
Todos
notas de voz
¿Buscas algo más sustancial? Consulte el código fuente de isowords, un juego de búsqueda de palabras para iOS integrado en SwiftUI y Composable Architecture.
Nota
Para obtener un tutorial interactivo paso a paso, asegúrese de consultar Conozca la arquitectura componible.
Para crear una característica usando la Arquitectura Composable, usted define algunos tipos y valores que modelan su dominio:
Estado : un tipo que describe los datos que su característica necesita para realizar su lógica y representar su interfaz de usuario.
Acción : un tipo que representa todas las acciones que pueden ocurrir en su función, como acciones de usuario, notificaciones, fuentes de eventos y más.
Reductor : una función que describe cómo hacer evolucionar el estado actual de la aplicación al siguiente estado dada una acción. El reductor también es responsable de devolver cualquier efecto que deba ejecutarse, como solicitudes de API, lo que se puede realizar devolviendo un valor Effect
.
Tienda : el tiempo de ejecución que realmente impulsa su función. Envía todas las acciones del usuario a la tienda para que la tienda pueda ejecutar el reductor y los efectos, y puede observar los cambios de estado en la tienda para que pueda actualizar la interfaz de usuario.
Los beneficios de hacer esto son que desbloqueará instantáneamente la capacidad de prueba de su función y podrá dividir funciones grandes y complejas en dominios más pequeños que se pueden unir.
Como ejemplo básico, considere una interfaz de usuario que muestra un número junto con los botones "+" y "-" que incrementan y disminuyen el número. Para hacer las cosas interesantes, supongamos que también hay un botón que, cuando se toca, realiza una solicitud de API para obtener un dato aleatorio sobre ese número y lo muestra en la vista.
Para implementar esta función, creamos un nuevo tipo que albergará el dominio y el comportamiento de la función, y se anotará con la macro @Reducer
:
importar ComposableArchitecture@Reducerstruct Característica {}
Aquí necesitamos definir un tipo para el estado de la característica, que consta de un número entero para el recuento actual, así como una cadena opcional que representa el hecho que se presenta:
@Reducerstruct Característica { @ObservableState struct Estado: Equatable { var count = 0 var numberFact: ¿Cadena? }}
Nota
Hemos aplicado la macro @ObservableState
a State
para aprovechar las herramientas de observación de la biblioteca.
También necesitamos definir un tipo para las acciones de la característica. Existen acciones obvias, como tocar el botón de disminución, el botón de incremento o el botón de hecho. Pero también hay algunos que no son tan obvios, como la acción que ocurre cuando recibimos una respuesta de la solicitud API de hecho:
@Reducerstruct Característica { @ObservableState struct Estado: Equatable { /* ... */ } enum Acción { case decrementButtonTapped case incrementButtonTapped case numberFactButtonTapped case numberFactResponse(String) }}
Y luego implementamos la propiedad body
, que es responsable de componer la lógica y el comportamiento reales de la característica. En él podemos usar el reductor Reduce
para describir cómo cambiar el estado actual al siguiente y qué efectos deben ejecutarse. Algunas acciones no necesitan ejecutar efectos y pueden devolver .none
para representar eso:
@Reducerstruct Característica { @ObservableState struct Estado: Equatable { /* ... */ } enum Acción { /* ... */ } var cuerpo: algún Reductor{ Reducir { estado, acción en cambiar de acción {caso .decrementButtonTapped: state.count -= 1 retorno .ninguno caso .incrementButtonTapped: state.count += 1 return .none case .numberFactButtonTapped: return .run { [count = state.count] enviar let (data, _) = intentar esperar URLSession.shared.data( de: URL (cadena: "http://numbersapi.com/(count)/trivia")! ) await enviar ( .numberFactResponse (String (decodificación: datos, como: UTF8.self)) ) } caso let .numberFactResponse (hecho): state.numberFact = retorno de hecho .none } } }}
Y finalmente definimos la vista que muestra la característica. Mantiene un StoreOf
para que pueda observar todos los cambios en el estado y volver a renderizar, y podemos enviar todas las acciones del usuario a la tienda para que el estado cambie:
estructura FeatureView: Ver { let store: StoreOfvar body: some View { Form { Sección { Text("(store.count)") Button("Decrement") { store.send(.decrementButtonTapped) } Button( "Incremento") { store.send(.incrementButtonTapped) } } Sección { Botón("Dato numérico") { store.send(.numberFactButtonTapped) } } if let fact = store.numberFact { Texto(hecho) } } }}
También es sencillo sacar un controlador UIKit de esta tienda. Puede observar los cambios de estado en la tienda en viewDidLoad
y luego completar los componentes de la interfaz de usuario con datos de la tienda. El código es un poco más largo que la versión SwiftUI, por lo que lo hemos contraído aquí:
clase FeatureViewController: UIViewController { let store: StoreOfinit(store: StoreOf ) { self.store = store super.init(nibName: nil, paquete: nil) } requerido init?(codificador: NSCoder) { fatalError("init(coder:) no se ha implementado") } anular func viewDidLoad() { super.viewDidLoad() let countLabel = UILabel() let decrementButton = UIButton() let incrementButton = UIButton() let factLabel = UILabel() // Omitido: agregar subvistas y configurar restricciones... observar { [yo débil] en guardia dejarse más {regresar} countLabel.text = "(self.store.text)" factLabel.text = self.store.numberFact } } @objc función privada incrementButtonTapped() { self.store.send(.incrementButtonTapped) } @objc función privada decrementButtonTapped() { self.store.send(.decrementButtonTapped) } @objc privado func factButtonTapped() { self.store.send(.numberFactButtonTapped) }}
Una vez que estemos listos para mostrar esta vista, por ejemplo en el punto de entrada de la aplicación, podemos construir una tienda. Esto se puede hacer especificando el estado inicial en el que iniciar la aplicación, así como el reductor que alimentará la aplicación:
importar ComposableArchitecture@mainstruct MiAplicación: Aplicación { cuerpo var: alguna escena { Grupo de ventanas { FeatureView( tienda: Tienda(estadoinicial: Característica.Estado()) { Característica() } ) } }}
Y eso es suficiente para tener algo en la pantalla con lo que jugar. Definitivamente son algunos pasos más que si hicieras esto con SwiftUI básico, pero hay algunos beneficios. Nos brinda una manera consistente de aplicar mutaciones de estado, en lugar de dispersar la lógica en algunos objetos observables y en varios cierres de acciones de componentes de la interfaz de usuario. También nos brinda una forma concisa de expresar los efectos secundarios. Y podemos probar inmediatamente esta lógica, incluidos los efectos, sin hacer mucho trabajo adicional.
Nota
Para obtener información más detallada sobre las pruebas, consulte el artículo de pruebas dedicado.
Para realizar pruebas, utilice TestStore
, que se puede crear con la misma información que Store
, pero realiza un trabajo adicional para permitirle afirmar cómo evoluciona su característica a medida que se envían las acciones:
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature() }}
Una vez creada la tienda de prueba, podemos usarla para hacer una afirmación de un flujo de pasos completo del usuario. En cada paso del camino debemos demostrar que ese estado cambió como esperábamos. Por ejemplo, podemos simular el flujo del usuario al tocar los botones de incrementar y disminuir:
// Pruebe que al tocar los botones de incremento/disminución se cambia el countawait store.send(.incrementButtonTapped) { $0.count = 1}esperar store.send(.decrementButtonTapped) { $0.cuenta = 0}
Además, si un paso provoca que se ejecute un efecto, que devuelve datos al almacén, debemos afirmarlo. Por ejemplo, si simulamos que el usuario toca el botón de hecho, esperamos recibir una respuesta de hecho con el hecho, lo que luego hace que se complete el estado numberFact
:
aguardar tienda.enviar(.numberFactButtonTapped)esperar tienda.receive(.numberFactResponse) { $0.númeroFact = ???}
Sin embargo, ¿cómo sabemos qué hecho se nos va a enviar?
Actualmente, nuestro reductor está utilizando un efecto que llega al mundo real para llegar a un servidor API, y eso significa que no tenemos forma de controlar su comportamiento. Dependemos de los caprichos de nuestra conectividad a Internet y la disponibilidad del servidor API para poder escribir esta prueba.
Sería mejor que esta dependencia se pasara al reductor para que podamos usar una dependencia activa al ejecutar la aplicación en un dispositivo, pero usar una dependencia simulada para las pruebas. Podemos hacer esto agregando una propiedad al reductor Feature
:
Característica @Reducerstruct { let numberFact: (Int) lanzamientos asíncronos -> Cadena //...}
Entonces podemos usarlo en la implementación reduce
:
caso .numberFactButtonTapped: return .run { [count = state.count] enviar let fact = try await self.numberFact(count) await send(.numberFactResponse(fact)) }
Y en el punto de entrada de la aplicación podemos proporcionar una versión de la dependencia que realmente interactúa con el servidor API del mundo real:
@mainstruct MiAplicación: Aplicación { var cuerpo: alguna escena { WindowGroup { FeatureView( tienda: Tienda(estadoinicial: Característica.Estado()) { Característica( numberFact: {número en let (datos, _) = intente esperar URLSession.shared.data( de: URL (cadena: "http://numbersapi.com/(número)")! ) devolver Cadena (decodificación: datos, como: UTF8.self) } ) } ) } }}
Pero en las pruebas podemos usar una dependencia simulada que devuelve inmediatamente un hecho determinista y predecible:
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature(numberFact: { "($0) es un buen número Brent" }) }}
Con ese poco de trabajo inicial, podemos finalizar la prueba simulando que el usuario toca el botón de hecho y luego recibe la respuesta de la dependencia para presentar el hecho:
aguardar tienda.enviar(.numberFactButtonTapped)esperar tienda.receive(.numberFactResponse) { $0.numberFact = "0 es un buen número Brent"}
También podemos mejorar la ergonomía del uso de la dependencia numberFact
en nuestra aplicación. Con el tiempo, la aplicación puede evolucionar hacia muchas funciones, y es posible que algunas de esas funciones también requieran acceso a numberFact
, y pasarla explícitamente a través de todas las capas puede resultar molesto. Existe un proceso que puede seguir para "registrar" dependencias en la biblioteca, haciéndolas disponibles instantáneamente para cualquier capa de la aplicación.
Nota
Para obtener información más detallada sobre la gestión de dependencias, consulte el artículo dedicado a las dependencias.
Podemos comenzar envolviendo la funcionalidad de hechos numéricos en un nuevo tipo:
estructura NumberFactClient { var fetch: (Int) lanzamientos asíncronos -> Cadena}
Y luego registrar ese tipo con el sistema de gestión de dependencias conformando el cliente al protocolo DependencyKey
, que requiere que usted especifique el valor en vivo que se usará al ejecutar la aplicación en simuladores o dispositivos:
extensión NumberFactClient: DependencyKey { static let liveValue = Self( buscar: { número en let (datos, _) = intentar esperar URLSession.shared .data(de: URL(cadena: "http://numbersapi.com/(número)")! ) devolver Cadena(decodificación: datos, como: UTF8.self) } )}extensión DependencyValues { var numberFact: NumberFactClient { get { self[NumberFactClient.self] } set { self[NumberFactClient.self] = nuevoValor } }}
Con ese poco de trabajo inicial realizado, puedes comenzar a utilizar instantáneamente la dependencia en cualquier característica usando el contenedor de propiedades @Dependency
:
@Reductor Característica de estructura {- let numberFact: (Int) lanzamientos asíncronos -> String+ @Dependency(.numberFact) var numberFact …- intente esperar self.numberFact(count)+ intente esperar self.numberFact.fetch(count) }
Este código funciona exactamente como antes, pero ya no es necesario pasar explícitamente la dependencia al construir el reductor de la característica. Al ejecutar la aplicación en vistas previas, el simulador o en un dispositivo, la dependencia en vivo se proporcionará al reductor y, en las pruebas, se proporcionará la dependencia de prueba.
Esto significa que el punto de entrada a la aplicación ya no necesita construir dependencias:
@mainstruct MiAplicación: Aplicación { var cuerpo: alguna escena { WindowGroup { FeatureView( tienda: Tienda(estadoinicial: Característica.Estado()) { Característica() } ) } }}
Y el almacén de prueba se puede construir sin especificar ninguna dependencia, pero aún puedes anular cualquier dependencia que necesites para el propósito de la prueba:
let store = TestStore(initialState: Feature.State()) { Característica()} withDependencies: { $0.numberFact.fetch = { "($0) es un buen número Brent" }}// ...
Estos son los conceptos básicos para crear y probar una característica en la Arquitectura Composable. Hay muchas más cosas por explorar, como la composición, la modularidad, la adaptabilidad y los efectos complejos. El directorio de ejemplos tiene una gran cantidad de proyectos para explorar y ver usos más avanzados.
La documentación para versiones y main
está disponible aquí:
main
1.17.0 (guía de migración)
1.16.0 (guía de migración)
1.15.0 (guía de migración)
1.14.0 (guía de migración)
1.13.0 (guía de migración)
1.12.0 (guía de migración)
1.11.0 (guía de migración)
1.10.0 (guía de migración)
1.9.0 (guía de migración)
1.8.0 (guía de migración)
1.7.0 (guía de migración)
1.6.0 (guía de migración)
1.5.0 (guía de migración)
1.4.0 (guía de migración)
1.3.0
1.2.0
1.1.0
1.0.0
0.59.0
0.58.0
0.57.0
Hay una serie de artículos en la documentación que pueden resultarle útiles a medida que se sienta más cómodo con la biblioteca:
Empezando
Dependencias
Pruebas
Navegación
Estado compartido
Actuación
concurrencia
Fijaciones
Si desea hablar sobre la arquitectura componible o tiene alguna pregunta sobre cómo usarla para resolver un problema en particular, hay varios lugares donde puede hablar con otros entusiastas de Point-Free:
Para debates extensos, recomendamos la pestaña de debates de este repositorio.
Para una charla informal, recomendamos la holgura de la comunidad sin puntos.
Puede agregar ComposableArchitecture a un proyecto Xcode agregándolo como una dependencia del paquete.
En el menú Archivo , seleccione Agregar dependencias del paquete...
Ingrese "https://github.com/pointfreeco/swift-composable-architecture" en el campo de texto URL del repositorio de paquetes
Dependiendo de cómo esté estructurado tu proyecto:
Si tiene un único destino de aplicación que necesita acceso a la biblioteca, agregue ComposableArchitecture directamente a su aplicación.
Si desea utilizar esta biblioteca desde varios objetivos de Xcode, o combinar objetivos de Xcode y objetivos de SPM, debe crear un marco compartido que dependa de ComposableArchitecture y luego depender de ese marco en todos sus objetivos. Para ver un ejemplo de esto, consulte la aplicación de demostración Tic-Tac-Toe, que divide muchas funciones en módulos y consume la biblioteca estática de esta manera usando el paquete Swift de tic-tac-toe .
La arquitectura componible se creó teniendo en cuenta la extensibilidad y hay una serie de bibliotecas respaldadas por la comunidad disponibles para mejorar sus aplicaciones:
Extras de Composable Architecture: una biblioteca complementaria de Composable Architecture.
TCAComposer: un marco macro para generar código repetitivo en la arquitectura componible.
TCACoordinators: El patrón coordinador en la Arquitectura Composable.
Si desea contribuir con una biblioteca, abra un PR con un enlace.
Las siguientes traducciones de este README han sido aportadas por miembros de la comunidad:
árabe
Francés
hindi
indonesio
italiano
japonés
coreano
Polaco
portugués
ruso
Chino simplificado
Español
ucranio
Si desea contribuir con una traducción, abra un PR con un enlace a un Gist.
Tenemos un artículo dedicado a todas las preguntas y comentarios más frecuentes que la gente tiene sobre la biblioteca.
Las siguientes personas dieron su opinión sobre la biblioteca en sus primeras etapas y ayudaron a convertirla en lo que es hoy:
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 y ¿Todos los suscriptores de Point-Free?
Un agradecimiento especial a Chris Liscio, quien nos ayudó a resolver muchas peculiaridades extrañas de SwiftUI y ayudó a perfeccionar la API final.
Y gracias a Shai Mishali y el proyecto CombineCommunity, del cual tomamos su implementación de Publishers.Create
, que usamos en Effect
para ayudar a unir las API basadas en delegaciones y devolución de llamadas, lo que facilita mucho la interfaz con marcos de trabajo de terceros.
La Arquitectura Composable se construyó sobre la base de ideas iniciadas por otras bibliotecas, en particular Elm y Redux.
También hay muchas bibliotecas de arquitectura en la comunidad Swift e iOS. Cada uno de ellos tiene su propio conjunto de prioridades y compensaciones que difieren de la Arquitectura Composable.
RIB
Bucle
rerápido
Flujo de trabajo
Kit de reactor
Comentarios de Rx
Mobius.swift
fluxor
PrometidoArquitecturaKit
Esta biblioteca se publica bajo la licencia MIT. Consulte LICENCIA para obtener más detalles.