Composable Architecture - A Fresh Look at Application Architecture

The balanced architecture of the mobile application prolongs the life of the project and developers.



History



Meet Alex. He needs to develop an application for making a shopping list. Alex is an experienced developer and first of all forms the requirements for the product:



  1. The ability to port the product to other platforms (watchOS, macOS, tvOS)
  2. Fully automated application regression
  3. IOS 13+ support


Alex recently got acquainted with the pointfree.com project , where Brandon and Stephen shared their lead on modern application architecture. This is how Alex found out about Composable Architecutre.



Composable Architecture



After reviewing the Composable Architecture documentation, Alex determined that he was dealing with a unidirectional architecture that matched the design requirements. From the brochure it followed:



  1. Dividing the project into modules;
  2. Data-driven UI - interface configuration is determined by its state;
  3. All module logic is covered by unit tests;
  4. Snapshot testing of interfaces;
  5. Supports iOS 13+, macOS, tvOS and watchOS;
  6. SwiftUI and UIKit support.


Before diving into the study of architecture, let's take a look at an object such as a smart umbrella.



image alt

How to describe the system by which the umbrella is arranged?



The umbrella system has four components:



. : .



. .



. .



. 10 .



composable architecture . .





? , .



UI β€” [];



Action β€” ;



State β€” [];



Environment β€” [ ];



Reducer β€” , [] ;



Effect β€” , action reducer.



( 1)







.



. , .



struct ShoppingListState {
    var products: [Product] = []
}

enum ShoppingListAction {
    case addProduct
}


reducer :



let shoppingListReducer = Reducer { state, action, env in
    switch action {
    case .addProduct:
        state.products.insert(Product(), at: 0)
        return .none
    }
}


:



struct Product {
    var id = UUID()
    var name = ""
    var isInBox = false
}

enum ProductAction {
    case toggleStatus
    case updateName(String)
}

let productReducer = Reducer { state, action, env in
    switch action {
    case .toggleStatus:
        state.isInBox.toggle()
        return .none
    case .updateName(let newName):
        state.name = newName
        return .none
    }
}


, reducer , , . reducer .



UI .



UI





iOS 13+ Composable Architecture SwiftUI, .



, Store:



typealias ShoppingListStore = Store<ShoppingListState, ShoppingListAction>

let store = ShoppingListStore(
    initialState: ShoppingListState(products: []),
    reducer: shoppingListReducer,
    environment: ShoppingListEnviroment()
)


Store viewModel MVVM β€” .



let view = ShoppingListView(store: store)

struct ShoppingListView: View {
    let store: ShoppingListStore

    var body: some View {
        Text("Hello, World!")
    }
}


Composable Architecture SwiftUI. , store ObservedObject, WithViewStore:



var body: some View {
    WithViewStore(store) { viewStore in
        NavigationView {
            Text("\(viewStore.products.count)")
            .navigationTitle("Shopping list")
            .navigationBarItems(
                trailing: Button("Add item") {
                    viewStore.send(.addProduct)
                }
            )
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}


Add item, . send(Action) .



, , :



struct ProductView: View {
    let store: ProductStore

    var body: some View {
        WithViewStore(store) { viewStore in
            HStack {
                Button(action: { viewStore.send(.toggleStatus) }) {
                    Image(
                        systemName: viewStore.isInBox
                            ? "checkmark.square"
                            : "square"
                    )
                }
                .buttonStyle(PlainButtonStyle())
                TextField(
                    "New item",
                    text: viewStore.binding(
                        get: \.name,
                        send: ProductAction.updateName
                    )
                )
            }
            .foregroundColor(viewStore.isInBox ? .gray : nil)
        }
    }
}




. ? .



enum ShoppingListAction {
        //       
    case productAction(Int, ProductAction)
    case addProduct
}

//      
// ..   ,   
let shoppingListReducer: Reducer<ShoppingListState, ShoppingListAction, ShoppingListEnviroment> = .combine(
        //  ,     
    productReducer.forEach(
                // Key path
        state: ShoppingListState.products,
                // Case path
        action: /ShoppingListAction.productAction,
        environment: { _ in ProductEnviroment() }
    ),
    Reducer { state, action, env in
        switch action {
        case .addProduct:
            state.products.insert(Product(), at: 0)
            return .none
                //      productReducer
        case .productAction:
            return .none
        }
    }
)




. .



UI :



var body: some View {
    WithViewStore(store) { viewStore in
        NavigationView {
            List {
        //   
                ForEachStore(
            //  store
                    store.scope(
                        state: \.products,
                        action: ShoppingListAction.productAction
                    ),
            //  
                    content: ProductView.init
                )
            }
            .navigationTitle("Shopping list")
            .navigationBarItems(
                trailing: Button("Add item") {
                    viewStore.send(.addProduct)
                }
            )
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}


150 , .





2 β€” (in progress)



Part 3 - expanding functionality, adding product removal and sorting (in progress)



Part 4 - add list caching and go to the store (in progress)



Sources



Product List Part 1: github.com



Approach authors portal: pointfree.com



Composable Architecture sources: https://github.com/pointfreeco/swift-composable-architecture




All Articles