Skip to content

Instantly share code, notes, and snippets.

@pitt500
Last active March 5, 2024 14:55
Show Gist options
  • Save pitt500/f5e32fccb575ce112ffea2827c7bf942 to your computer and use it in GitHub Desktop.
Save pitt500/f5e32fccb575ce112ffea2827c7bf942 to your computer and use it in GitHub Desktop.
An Spanish translation of The Composable Architecture's README.md

The Composable Architecture

CI

The Composable Architecture(o simplemente TCA) es una librería para construir aplicaciones de una manera consistente y entendible, teniendo en mente composición, pruebas y ergonomía. Puede ser utilizada en SwiftUI, UIKit, y en cualquier plataforma de Apple (iOS, macOS, tvOS, y watchOS).

¿Que esThe Composable Architecture?

Esta librería provee algunas herramientas básicas que pueden ser usadas para construir aplicaciones de diversa finalidad y complejidad. Provee casos de uso convincentes que se pueden seguir para resolver diferentes problemas que encuentras en tu día a día cuando construyes aplicaciones, tales como:

  • Manejo del estado
    Como gestionar el estado de tu aplicación usando simplemente tipos valor (structs y enums), y compartir el estado entre varias pantallas, de modo que la mutación del estado hecha en una pantalla puede ser observada inmediatamente en otra.

  • Composición
    Como desglosar features grandes en pequeños componentes que puedan ser extraídos a sus propios módulos asilados, y ser pegados de vuelta para formar la funcionalidad completa.

  • Efectos secundarios
    Como dejar que ciertas partes de la aplicación "hablen" con el mundo exterior de la manera más testeable y entendible posible.

  • Pruebas
    Como no solo probar un feature implementado en la arquitectura, sino también escribir pruebas de integración para los features que han sido compuestos por muchos elementos, y escribir pruebas end-to-end (de extremo a extremo) para entender como los efectos secundarios influyen en tu aplicación. Esto permite garantizar de manera sólida que tu lógica de negocio está funcionando de la manera que esperas.

  • Ergonomía
    Como lograr todo lo anterior en una API sencilla con la menor cantidad posible de conceptos y partes en movimiento.

Aprender más

The Composable Architecture fue diseñado a lo largo de muchos episodios enPoint-Free,una serie de videos dedicados a programación funcional y el lenguaje Swift, presentado porBrandon WilliamsyStephen Celis.

Puedes mirar todos los episodiosaquí,así como un tour dedicado a explorar la arquitectura desde cero:parte 1,parte 2,parte 3yparte 4.

imagen video poster

Ejemplos

Screenshots de aplicaciones de ejemplo

Este repo contienemuchosejemplos para demostrar como resolver problemas comunes y complejos con TCA. Consultaestedirectorio para ver todos ellos, incluyendo:

¿Buscas algo más en serio? Consulta el código fuente deisowords,un juego de búsqueda de palabras para iOS implementado en SwiftUI y TCA.

Uso básico

Para implementar un feature utilizando TCA, debes definir tipos y valores que modelarán tu dominio:

  • Estado:Un tipo que describe la información que tu feature necesita para ejecutar su lógica y ser mostrado en la pantalla.
  • Acción:Un tipo que representa todas las acciones que pueden pasar en un feature, tal como una acción del usuario, notificaciones, fuentes de eventos y más.
  • Ambiente:Un tipo que contiene las dependencias que un feature necesita, tal como llamadas a APIs, analíticas, etc.
  • Reducer (o Reductor):Una función que describe como va a evolucionar el estado actual de tu app hacia el siguiente estado dada una acción. El reducer es tambien responsable de regresar efectos que deban ser ejecutados, tal como llamadas a APIs, las cuales pueden ser hechas regresando un valorEffect.
  • Store:El lugar donde se almacena la información de un feature. Se envían todas las acciones del usuario al Store, y el mismo se encarga de ejecutar el reducer y los efectos, así como observar cambios de estado que actualizarán la UI.

Los beneficios de esto es que se obtiene instantánteamente una gran capacidad de prueba para tu funcionalidad, y de dividir un feature grande y complejo en dominios mas pequeños que luego puedan ser unidos.

Como un ejemplo básico, considera una UI que muestre un número junto con botones de "+" y "-" que lo incrementen y decrementen. Para hacer las cosas interesantes, supón que hay además un botón que al ser presionado hace una llamada a una API para obtener una curiosidad del número y mostrarla en una alerta.

El estado de este feature consistiría en un entero para el contador actual, así como un String opcional que represente el dato curioso en un título para la alerta que queremos mostrar (es opcional dado quenilrepresenta el no mostrar la alerta):

structAppState:Equatable{
varcount=0
varnumberFactAlert:String?
}

Después tenemos las acciones en el feature. Hay acciones obvias, tal como presionar el botón de incrementar, decrementar o el de la curiosidad, pero hay además otras no tan obvias, tal como la acción de el usuario cerrando la alerta, y la acción que ocurre cuando recibimos una respuesta de la API que devuelve la curiosidad del número:

enumAppAction:Equatable{
casefactAlertDismissed
casedecrementButtonTapped
caseincrementButtonTapped
casenumberFactButtonTapped
casenumberFactResponse(TaskResult<String>)
}

Lo siguiente es modelar el ambiente de dependencias que este feature necesita para llevar a cabo su trabajo. En particular, parar obtener la curiosidad del número necesitamos modelar una función asíncrona y lanzable con parametroInty valor de regresoString.

structAppEnvironment{
varnumberFact:(Int)asyncthrows->String
}

Después, debemos crear un reducer que implemente la lógica para este dominio. El reducer describe como cambiar el estado actual al siguiente estado, y describe que efectos necesitan ser ejecutados. Algunas acciones no necesitan ejecutar efectos, por lo que se puede regresar.nonepara representarlo:

letappReducer=Reducer<AppState,AppAction,AppEnvironment>{state,action,environmentin
switch action{
case.factAlertDismissed:
state.numberFactAlert=nil
return.none

case.decrementButtonTapped:
state.count-=1
return.none

case.incrementButtonTapped:
state.count+=1
return.none

case.numberFactButtonTapped:
return.task{
await.numberFactResponse(TaskResult{tryawaitenvironment.numberFact(state.count)})
}

caselet.numberFactResponse(.success(fact)):
state.numberFactAlert=fact
return.none

case.numberFactResponse(.failure):
state.numberFactAlert="No pudimos cargar una curiosidad para este número:("
return.none
}
}

Y finalmente, definimos la vista que mostrará el feature. La vista tendrá una propiedadStore<AppState, AppAction>que va a observar todos los cambios del estado y volver a actualizar la UI. Podemos enviar todas las acciones del usuario al store para que el estado cambie. Debemos además crear unstruct wrappersobre la alerta de curiosidad para hacerlaIdentifiable,por lo cual, el modificador.alertrequiere:

structAppView:View{
letstore:Store<AppState,AppAction>

varbody:someView{
WithViewStore(self.store){viewStorein
VStack{
HStack{
Button(""){viewStore.send(.decrementButtonTapped)}
Text("\(viewStore.count)")
Button("+"){viewStore.send(.incrementButtonTapped)}
}

Button("Curiosidad del número"){viewStore.send(.numberFactButtonTapped)}
}
.alert(
item:viewStore.binding(
get:{$0.numberFactAlert.map(FactAlert.init(title:))},
send:.factAlertDismissed
),
content:{Alert(title:Text($0.title))}
)
}
}
}

structFactAlert:Identifiable{
vartitle:String
varid:String{self.title}
}

Es importante destacar que fuimos capaces de implementar este feature por completo sin tener un efecto real directamente. Esto es muy importante ya que significa que los features pueden ser implementados aislados de sus dependencias, lo cual ayuda a mejorar el tiempo de compilación.

Es también muy fácil tener un controlador de UIKit fuera del store. Te subscribes al store enviewDidLoadpara poder actualizar la UI y mostrar alertas. El código es un poco más largo que en la versión de SwiftUI, por lo que lo colapsamos aquí:

¡Click para expandir!
classAppViewController:UIViewController{
letviewStore:ViewStore<AppState,AppAction>
varcancellables:Set<AnyCancellable>=[]

init(store:Store<AppState,AppAction>){
self.viewStore=ViewStore(store)
super.init(nibName:nil,bundle:nil)
}

requiredinit?(coder:NSCoder){
fatalError("init(coder:) no ha sido implementado")
}

overridefuncviewDidLoad(){
super.viewDidLoad()

letcountLabel=UILabel()
letincrementButton=UIButton()
letdecrementButton=UIButton()
letfactButton=UIButton()

// Omitido: Agregar subviews y configurar constrains...

self.viewStore.publisher
.map{"\($0.count)"}
.assign(to:\.text,on:countLabel)
.store(in:&self.cancellables)

self.viewStore.publisher.numberFactAlert
.sink{[weak self]numberFactAlertin
letalertController=UIAlertController(
title:numberFactAlert,message:nil,preferredStyle:.alert
)
alertController.addAction(
UIAlertAction(
title:"Ok",
style:.default,
handler:{_inself?.viewStore.send(.factAlertDismissed)}
)
)
self?.present(alertController,animated:true,completion:nil)
}
.store(in:&self.cancellables)
}

@objcprivatefuncincrementButtonTapped(){
self.viewStore.send(.incrementButtonTapped)
}
@objcprivatefuncdecrementButtonTapped(){
self.viewStore.send(.decrementButtonTapped)
}
@objcprivatefuncfactButtonTapped(){
self.viewStore.send(.numberFactButtonTapped)
}
}

Una vez que estamos listos para mostrar la vista, podemos construir un store, por ejemplo, desde el punto de entrada de la app. Este es el momento donde necesitamos pasar las dependencias, incluyendo el endpoint denumberFactque está obteniendo la información desde el mundo real:

@main
structCaseStudiesApp:App{
varbody:someScene{
AppView(
store:Store(
initialState:AppState(),
reducer:appReducer,
environment:AppEnvironment(
numberFact:{numberin
let(data,_)=tryawaitURLSession.shared
.data(from:.init(string:"http://numbersapi /\(number)")!)
returnString(decoding:data,using:UTF8.self)
}
)
)
)
}

Y esto es suficiente para ver algo en la pantalla con que jugar. Definitivamente serían solo unos cuantos pasos más si hicieras esto en SwiftUI vainilla, pero hay algunos beneficios. TCA nos brinda una manera consistente de aplicar mutaciones al estado, en lugar de tener lógica esparcida por variosobservable objectsyclosuresen los componentes de la UI. Además, también ganamos una forma consistente de expresar efectos secundarios. Y finalmente, podemos probar nuestra lógica fácilmente, incluyendo los efectos sin tener que hacer trabajo adicional.

Pruebas

Para probar, primero crea unTestStorecon la misma información que unStorenormal, excepto que esta vez podemos pasar dependencias adecuadas para probar. Particularmente, ahora podemos usar una implementación denumberFactque devuelve inmediatamente un valor que nosotros controlamos en lugar de tener que esperar uno del mundo real.

@MainActor
functestFeature()async{
letstore=TestStore(
initialState:AppState(),
reducer:appReducer,
environment:AppEnvironment(
numberFact:{"\($0)es un gran número Brent"}
)
)
}

Una vez que la prueba es creada, podemos usarla para comprobar un flujo de pasos hechos por el usuario. En cada paso, necesitamos comprobar que el estado ha cambiado tal cual esperamos. Además, si un paso hace que se ejecute un efecto que mande datos alstore,debemos corroborar que esas acciones se recibieron correctamente.

La siguiente prueba hace que el usuario incremente y decremente el conteo, entonces se pregunta por la curiosidad de ese número, y la respuesta de ese efecto dispara una alerta para ser mostrada, y finalmente, al hacerdismissla alerta desaparece.

// Prueba de como al presionar los botones de incrementar y decrementar el contador cambia
awaitstore.send(.incrementButtonTapped){
$0.count=1
}
awaitstore.send(.decrementButtonTapped){
$0.count=0
}

// Prueba de como al presionar el boton de la curiosidad recibimos una respuesta desde el efecto.
// Nota que tenemos que esperar la respuesta ya que el efecto es asincrono y toma una pequeña
// cantidad de tiempo para ser emitido.
awaitstore.send(.numberFactButtonTapped)

awaitstore.receive(.numberFactResponse(.success("0 es un gran número Brent"))){
$0.numberFactAlert="0 es un gran número Brent"
}

// Y finalmente cerramos la alerta
awaitstore.send(.factAlertDismissed){
$0.numberFactAlert=nil
}

Esto es lo básico para implementar y probar un feature en TCA. Haymuchasmás cosas por explorar, tal como composición, modularidad, adaptabilidad y efectos complejos. El directorio deejemplostiene varios proyectos que puedes explorar para ver otros usos avanzados.

Debugging

TCA viene con un gran número de herramientas que nos ayudan en eldebugging.

  • reducer.debug()imprime en la pantalla cada acción que elreducerrecibe y cada mutación hecha en el estado.

    received action:
    AppAction.todoCheckboxTapped(id: UUID(5834811A-83B4-4E5E-BCD3-8A38F6BDCA90))
      AppState(
      todos: [
      Todo(
    -isComplete: false,
    +isComplete: true,
      description: "Milk",
      id: 5834811A-83B4-4E5E-BCD3-8A38F6BDCA90
      ),
     … (2 unchanged)
      ]
      )
  • reducer.signpost()instrumenta unreducercon señales para poder obtener información sobre cuanto tardan en ejecutarse las acciones y cuando se ejecutan los efectos.

Librerías complementarias

Uno de los principios más importantes de TCA es que los efectos secundarios nunca son ejecutados directamente, sino que se encapsulan en el tipoEffect,regresado por losreducers,y luego elStoreejecuta el efecto. Eso es crucial para simplificar la forma en que los datos fluyen a través de una aplicación y para poder probar el ciclo completo de acciones del usario end-to-end.

Sin embargo, esto también significa que muchas librerias y SDKs con las que interactuas diariamente necesitan actualizarse para ser un poco más amigables con el estilo TCA. Es por eso que nos gustaría aliviar el dolor de usar algunos de los frameworks mas populares de Apple al proporcionar libreríaswrapperque expongan su funcionalidad de una manera que se adapte bien a nuestra librería. Hasta ahora tenemos:

  • ComposableCoreLocation:Un wrapper deCLLocationManagerque facilita su uso en unreducer,y el escribir pruebas sobre cómo tu lógica interactúa con la funcionalidad deCLLocationManager.

  • ComposableCoreMotion:Un wrapper deCMMotionManagerque facilita su uso en unreducer,y el escribir pruebas sobre cómo tu lógica interactúa con la funcionalidad deCMMotionManager.

  • Más librerias vendrán pronto. ¡Manténganse al tanto! 😉

Si estás interesado en contribuir para crear una librería wrapper de algún framework que no hayamos cubierto, siéntete libre de abrir un issue explicando tu interés para para que podamos discutirlo.

Preguntas frecuentes

  • ¿Cómo se compara TCA con Elm, Redux y otras?

    Expandir para ver la respuesta TCA se basa en ideas fundadas de la arquictectura Elm (TEA) y Redux, pero hechas para sentirse como en casa en el lenguaje Swift y en las plataformas de Apple.

    De alguna forma, TCA es un poco más estricto que otras librerías. Por ejemplo, Redux no explica como se deben ejecutar los efectos secundarios, pero TCA requiere que todos los efectos secundarios sean modealados en el tipoEffectand regresado desde el reducer.

    En otras, TCA es más relajado que otras librerías. Por ejemplo, Elm controla qué tipos de efectos se pueden crear a través del tipoCmd,pero TCA permite regresar cualquier tipo de efecto, ya queEffectconforma el protocoloPublisherde Combine.

    Y además, hay ciertas cosas que TCA prioriza mucho y que no son puntos de enfoque para Redux, Elm o la mayoria de otras librerías. Por ejemplo, la composición es un aspecto muy importante de TCA, que es el proceso de dividir features grandes en unidades más pequeñas que se puedan unir. Esto se logra mediante los operadorespullbackycombineen los reducers, y ayuda en el manejo de features complejos, así como en la modularización para una código mejor aislado y mejorar los tiempos de compilación.

Requisitos

TCA depende del framework de Combine, por lo que el deployment target mínimo requerido es iOS 13, macOS 10.15, Mac Catalyst 13, tvOS 13, y watchOS 6. Si tu aplicación tiene que dar soporte a versiones de sistemas operativos más antiguas, hay forks paraReactiveSwiftyRxSwiftque puedes utilizar.

Instalación

Puedes añadir ComposableArchitecture a un proyecto de Xcode agregándolo como un paquete de Swift:

  1. Desde el menúArchivo,seleccionaAñadir paquetes...
  2. Introduce "https://github /pointfreeco/swift-composable-architecture"en el campo de texto de la url del repositorio.
  3. Dependiendo de como esté estructurado tu proyecto:
    • Si tienes un solo target que necesite acceso a la librería, solo agregaComposableArchitecturedirectamente a tu aplicación.
    • Si quieres usar esta librería en múltiples targets de Xcode, o mezclar targets de Xcode con otros targets de SPM (Swift Package Manager), debes crear un framework compartido que dependa deComposableArchitecturey luego hacer que tus targets dependan de él. Si quieres ver un ejemplo de esto, mira el demo deTic-Tac-Toe,ya que en él se dividen muchos features en módulos y consumen la librería estática de esta manera en el paquete de Swifttic-tac-toe.

Documentación

La documentación de cada release ymainestá disponible aquí:

Otras versiones

Ayuda

Si quisieras discutir más sobre TCA o tienes alguna pregunta sobre como usarlo en un problema específico, puedes crear un tema en la pestaña dediscusiones (o issues)de este repo, o preguntar en elforo de swift.org.

Traducciones

Las siguentes traducciones de este README han sido contribuidas por parte de miembros de la comunidad:

Si quisieras contribuir con las traducciones, por favorabre un PRcon a link a unGist.

Créditos y agradecimientos

Las siguientes personas dieron feedback a la librería en su etapa inicial y ayudaron a hacerla lo que es hoy en día:

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 a todos los subscriptores dePoint-Free😁.

Agradecimientos especiales paraChris Liscioquien nos ayudó resolviendo varias peculiaridades con SwiftUI y al refinamiento de la API final.

Y gracias aShai Mishaliy al proyecto deCombineCommunity,de donde tomamos su implementacion dePublishers.Create,la cual usamos enEffectpara ayudar a unir APIs basadas en delegados y callbacks, lo que facilitó mucho la interfaz con frameworks de terceros.

Otras librerías

TCA fue implementado bajo la fundación de ideas iniciadas en otras librerías, particularmenteElmyRedux.

Hay muchas otras librerías en la comunidad de Swift y iOS. Cada una de ellas tiene su propio conjunto de ventajas y desventajas que difieren de TCA.

Licencia

Esta lbrería es publicada bajo la licencia del MIT. VerLICENCIApara más detalles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment