Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Composable Architecture: A great (and OPINI...

The Composable Architecture: A great (and OPINIONATED) architecture to build scalable apps

The Composable Architecture (or TCA for short), is an architecture/library for building large, robust, and scalable applications. It’s built on a foundation of ideas popularized by Elm and Redux, but made to feel at home in the Swift language and on Apple's platforms. Further, it’s strongly opinionated about how to manage side effects and build end-to-end tests that guarantee your business logic is doing what is expected. In this talk, we are going to describe the benefits vs other architectures and build a demo on SwiftUI applying the main concepts.

Avatar for Pedro Rojas

Pedro Rojas

October 12, 2022
Tweet

Other Decks in Programming

Transcript

  1. The Composable Architecture: A great (and OPINIONATED) architecture to build

    scalable apps! iOS Developers
 Global Summit'22 By Pedro Rojas (aka Pitt)
  2. About me 👪 Father of two kids 👨💻 SWE at

    Meta 📺 Youtube @swiftandtips 🌎
  3. About me 👪 Father of two kids 👨💻 SWE at

    Meta 📺 Youtube @swiftandtips 🎙 Podcast @letswiftpodcast 🌎 🇲🇽🇪🇸🇨🇴🇦🇷🇨🇱🇪🇨
  4. func receiveItems(data: [Item]) { items = data // ... more

    work } // Something invoked this... func restoreItems() { items = [] } . . . Unexpected!
  5. TCA

  6. View Action Reducer State Store sends received by mutates triggers

    update (Presenter) (Enum) (Function) (Struct)
  7. View Action Reducer State Effect Store sends received by returns

    mutates triggers update (Presenter) (Enum) (Function) (Struct) (Outside World)
  8. View Action Reducer State Effect Store sends sends received by

    returns mutates triggers update (Presenter) (Enum) (Function) (Struct) (Outside World)
  9. View Action Reducer State Environment Effect Store sends sends received

    by returns interacts with mutates triggers update (Presenter) (Enum) (Function) (Struct) (Dependencies) (Outside World)
  10. let reducer = Reducer< State, Action, Environment > { state,

    action, environment in switch action { case .increaseCounter: state.counter += 1 return Effect.none case .decreaseCounter: state.counter -= 1 return Effect.none } } Reducer
  11. struct ContentView: View { let store: Store<State, Action> var body:

    some View { WithViewStore(self.store) { viewStore in // SwiftUI View . . . } } } View
  12. { fetch products Open Cart Add item to cart Select

    Products tab Select Profile tab Increase/decrease 
 product qty
  13. View Action Reducer State Environment Effect Store sends sends received

    by returns interacts with mutates triggers update (Presenter) (Enum) (Function) (Struct) (Dependencies) (Outside World)
  14. View Action Reducer State Environment Effect Store sends sends received

    by returns interacts with mutates triggers update (Presenter) (Enum) (Function) (Struct) (Dependencies) (Outside World)
  15. struct AddToCartDomain { struct State: Equatable { var count =

    0 } enum Action: Equatable { case didTapPlusButton case didTapMinusButton } struct Environment {} static let reducer = Reducer< State, Action, Environment > { state, action, environment in // ... } } Domain
  16. View struct AddToCartButton: View { let store: Store<AddToCartDomain.State, AddToCartDomain.Action> var

    body: some View { WithViewStore(self.store) { viewStore in // SwiftUI View . . . } } } }
  17. View if viewStore.count > 0 { PlusMinusButton(store: self.store) } else

    { Button { viewStore.send(.didTapPlusButton) } label: { Text("Add to Cart") .padding(10) .background(.blue) .foregroundColor(.white) .cornerRadius(10) } .buttonStyle(.plain) }
  18. View if viewStore.count > 0 { PlusMinusButton(store: self.store) } else

    { Button { viewStore.send(.didTapPlusButton) } label: { Text("Add to Cart") .padding(10) .background(.blue) .foregroundColor(.white) .cornerRadius(10) } .buttonStyle(.plain) }
  19. struct ProductDomain { struct State: Equatable, Identifiable { let id:

    UUID let product: Product var count: Int = 0 var addToCartState = AddToCartDomain.State() } //. . . } State
  20. struct ProductDomain { struct State: Equatable, Identifiable { let id:

    UUID let product: Product var count: Int = 0 var addToCartState = AddToCartDomain.State() } //. . . } State
  21. static let reducer = Reducer< State, Action, Environment > Reducer

    .init { state, action, environment in switch action { case .addToCart(.didTapPlusButton): state.count += 1 return .none case .addToCart(.didTapMinusButton): state.count = max(0, state.count - 1) return .none } }
  22. static let reducer = Reducer< State, Action, Environment > Reducer

    .init { state, action, environment in switch action { case .addToCart(.didTapPlusButton): state.count += 1 return .none case .addToCart(.didTapMinusButton): state.count = max(0, state.count - 1) return .none } }
  23. static let reducer = Reducer< State, Action, Environment > Reducer

    .init { state, action, environment in switch action { case .addToCart(.didTapPlusButton): state.count += 1 return .none case .addToCart(.didTapMinusButton): state.count = max(0, state.count - 1) return .none } } .combine( )
  24. static let reducer = Reducer< State, Action, Environment > Reducer

    .init { state, action, environment in switch action { case .addToCart(.didTapPlusButton): state.count += 1 return .none case .addToCart(.didTapMinusButton): state.count = max(0, state.count - 1) return .none } } .combine( ) AddToCartDomain.reducer .pullback( state: \.addToCartState, action: /ProductDomain.Action.addToCart, environment: { _ in AddToCartDomain.Environment() } ),
  25. State struct ProductListDomain { struct State: Equatable { var productListState:

    IdentifiedArrayOf<ProductDomain.State> = [] // . . . } // . . . }
  26. State struct ProductListDomain { struct State: Equatable { var productListState:

    IdentifiedArrayOf<ProductDomain.State> = [] // . . . } // . . . }
  27. Reducer static let reducer = Reducer< State, Action, Environment >.combine(

    .init { state, action, environment in switch action { case .fetchProducts: return .none case .product(let id, let action): return .none // . . . } )
  28. Reducer static let reducer = Reducer< State, Action, Environment >.combine(

    .init { state, action, environment in switch action { case .fetchProducts: return .none case .product(let id, let action): return .none // . . . } ) ProductDomain.reducer.forEach( state: \.productListState, action: /ProductListDomain.Action.product(id:action:), environment: { _ in ProductDomain.Environment() } ),
  29. Reducer static let reducer = Reducer< State, Action, Environment >.combine(

    .init { state, action, environment in switch action { case .fetchProducts: return .none case .product(let id, let action): return .none // . . . } ) ProductDomain.reducer.forEach( state: \.productListState, action: /ProductListDomain.Action.product(id:action:), environment: { _ in ProductDomain.Environment() } ), state.productListState = IdentifiedArrayOf( uniqueElements: environment.fetchProducts().map { ProductDomain.State( id: environment.uuid(), product: $0 ) } )
  30. View struct ProductListView: View { let store: Store<ProductListDomain.State,ProductListDomain.Action> var body:

    some View { WithViewStore(self.store) { viewStore in NavigationView { List { ForEachStore( self.store.scope( state: \.productListState, action: ProductListDomain.Action .product(id: action:) ) ) { ProductCell(store: $0) } } .onAppear { viewStore.send(.fetchProducts) } // . . .
  31. View struct ProductListView: View { let store: Store<ProductListDomain.State,ProductListDomain.Action> var body:

    some View { WithViewStore(self.store) { viewStore in NavigationView { List { ForEachStore( self.store.scope( state: \.productListState, action: ProductListDomain.Action .product(id: action:) ) ) { ProductCell(store: $0) } } .onAppear { viewStore.send(.fetchProducts) } // . . .
  32. struct Environment { var fetchProducts: @Sendable () async throws ->

    [Product] var sendOrder: @Sendable ([CartItem]) async throws -> String var uuid: @Sendable () -> UUID } Environment
  33. Reducer .init { state, action, environment in switch action {

    case .fetchProducts: return .task { await .fetchProductsResponse( TaskResult { try await environment.fetchProducts() } ) } case .fetchProductsResponse(.success(let products)): state.productListState = IdentifiedArrayOf( uniqueElements: products.map { ProductDomain.State( id: environment.uuid(), product: $0 ) } ) return .none case .fetchProductsResponse(.failure(let error)): print("Error getting products, try again later.") return .none // . . .
  34. Reducer .init { state, action, environment in switch action {

    case .fetchProducts: return .task { await .fetchProductsResponse( TaskResult { try await environment.fetchProducts() } ) } case .fetchProductsResponse(.success(let products)): state.productListState = IdentifiedArrayOf( uniqueElements: products.map { ProductDomain.State( id: environment.uuid(), product: $0 ) } ) return .none case .fetchProductsResponse(.failure(let error)): print("Error getting products, try again later.") return .none // . . .
  35. Reducer // . . . case .cart(let action): switch action

    { // . . . case .cartItem(_, let action): switch action { case .deleteCartItem(let product): return .task { .resetProduct(product: product) } } default: return .none } // . . .
  36. func testFetchProductsSuccess() async { let products: [Product] = […] let

    store = TestStore( initialState: ProductListDomain.State(), reducer: ProductListDomain.reducer, environment: ProductListDomain.Environment( fetchProducts: { products }, sendOrder: { _ in fatalError("unimplemented") }, uuid: { UUID.newUUIDForTest } ) ) // . . .
  37. func testFetchProductsSuccess() async { let products: [Product] = […] let

    store = TestStore( initialState: ProductListDomain.State(), reducer: ProductListDomain.reducer, environment: ProductListDomain.Environment( fetchProducts: { products }, sendOrder: { _ in fatalError("unimplemented") }, uuid: { UUID.newUUIDForTest } ) ) // . . . await store.send(.fetchProducts) { $0.dataLoadingStatus = .loading } await store.receive(.fetchProductsResponse(.success(products))) { $0.productListState = identifiedArray $0.dataLoadingStatus = .success } }
  38. struct FeatureState { … } enum FeatureAction { … }

    struct FeatureEnvironment { var client: Client; … } let featureReducer = Reducer< FeatureState, FeatureAction, FeatureEnvironment > { state, action, environment in … }
  39. struct FeatureState { … } enum FeatureAction { … }

    struct FeatureEnvironment { var client: Client; … } let featureReducer = Reducer< FeatureState, FeatureAction, FeatureEnvironment > { state, action, environment in … } struct Feature: ReducerProtocol { struct State { … } enum Action { … } let client: Client func reduce(into state: inout State, action: Action) -> Effect<Action, Never> { … } }
  40. “Almost everything we know about good software architecture has to

    do with making software easy to change.” - Mary Poppendieck
  41. Contact Thank you, have a great day! ☺ 📺 Youtube

    @swiftandtips 🎙 Podcast @letswiftpodcast Twitter @swiftandtips Demo