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:
- The ability to port the product to other platforms (watchOS, macOS, tvOS)
- Fully automated application regression
- 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:
- Dividing the project into modules;
- Data-driven UI - interface configuration is determined by its state;
- All module logic is covered by unit tests;
- Snapshot testing of interfaces;
- Supports iOS 13+, macOS, tvOS and watchOS;
- SwiftUI and UIKit support.
Before diving into the study of architecture, let's take a look at an object such as a smart umbrella.
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