ReSwift is one of the most popular frameworks that provides the components to build an iOS Redux-like application in a very easy way.
Unfortunately, it brings also some issues, for example it's impossible to create a real modularised application because every component relies on a global action.
This talk wants to propose a solution to this problem, implementing in a very easy way some extensions to ReSwift, taking inspiration from the Pointfree Composable Architecture, in particular we will see how the pullback function can be used in an old ReSwift Application in order to bring our projects to the next level.
20. Redux: why?
• Unidirectional Data Flow: it’s easier to debug
• Code organization: it’s easier to get started
• Logic in reducers: it’s easier to test
21. ReSwift: why?
• The most starred framework to implement a redux architecture
22. ReSwift: why?
• The most starred framework to implement a redux architecture
• Platform independent
36. View
struct CatListView: View {
@ObservedObject var viewModel: CatListViewModel
init(store: Store<AppState>) {
viewModel = CatListViewModel(store: store)
}
var body: some View {
// view implementation
View
Store
Reducer
Action
37. View Model
class CatListViewModel: ObservableObject {
let store: Store<AppState>
@Published var isLoading: Bool
@Published var list: [CatPresentationModel]
init(store: Store<AppState>) {
self.store = store
isLoading = store.state.catListState.loading
list = store.state.catListState.list
}
}
View
Store
Reducer
Action
40. Actions: Middleware
let catListMiddleware: Middleware<AppState>
= createThunkMiddleware()
View
Store
Reducer
Action
let fetchCats = Thunk<CatListState> { dispatch, getState in
dispatch(CatListAction.Fetch())
Task { @MainActor in
let list = await fetchCatList()
.map { $0.compactMap(CatPresentationModel.init) }
switch list {
case let .success(list):
dispatch(CatListAction.SetList(list: list))
...
41. Reducer
public func catListReducer(action: Action, state: CatListState?) -> CatListState {
var state = state ?? .init(list: [], error: nil, loading: false)
switch action {
case _ as CatListAction.Fetch:
state = .init(list: [], error: nil, loading: true)
case let event as CatListAction.SetList:
state = .init(list: event.list, error: nil, loading: false)
default:
break
}
return state
}
View
Store
Reducer
Action
42. View Model
extension CatListViewModel: StoreSubscriber {
typealias StoreSubscriberStateType = CatListState
func newState(state: CatListState) {
isLoading = state.loading
list = state.list
}
}
View
Store
Reducer
Action
43. App
var store = Store<AppState>(reducer: appReducer,
state: nil,
middleware: [catListMiddleware])
@main
struct CatsApp: App {
var body: some Scene {
WindowGroup {
CatListView(store: store)
}
}
}
47. Let’s start modularizing!
1. Create a new Swift Package
2. Move all the CatList files inside the new package
3. Build the new package
48. Let’s start modularizing!
1. Create a new Swift Package
2. Move all the CatList files inside the new package
3. Build the new package
4. Build the main Application
49. Create a new Swift Package
• Create a new Swift Package called CatList
let package = Package(
name: "CatList"
)
50. Create a new Swift Package
• Create a new Swift Package called CatList
• Set the Swift package supported platforms
let package = Package(
name: "CatList",
platforms: [.iOS(.v15)],
)
51. Create a new Swift Package
• Create a new Swift Package called CatList
• Set the Swift package supported platforms
• Add the ReSwift and ReSwiftThunk dependencies
let package = Package(
name: "CatList",
platforms: [.iOS(.v15)],
dependencies: [
.package(name: "ReSwift")
.package(name: "ReSwiftThunk")
],
)
52. Create a new Swift Package
• Create a new Swift Package called CatList
• Set the Swift package supported platforms
• Add the ReSwift and ReSwiftThunk dependencies
• Add the Network dependency
let package = Package(
name: "CatList",
platforms: [.iOS(.v15)],
dependencies: [
.package(name: "ReSwift")
.package(name: "ReSwiftThunk"),
.package(name: "Network")
],
)
53. Move the CatList files in the new Package
• Drag the
fi
les in the new package
54. Move the CatList files in the new Package
• Drag the
fi
les in the new package
• Add the missing imports where needed
57. class CatListViewModel: ObservableObject {
let store: Store<AppState>
...
}
Cannot
fi
nd type 'AppState' in scope
init(store: Store<AppState>) {
viewModel = CatListViewModel(store: store)
}
let catListMiddleware: Middleware<AppState>
= createThunkMiddleware()
let fetchCats = Thunk<AppState> { dispatch, getState in
Build the new package
60. Build the new package
Middleware<AppState> Middleware<CatListState>
Store<AppState> Store<CatListState>
61. Build the new package
Middleware<AppState> Middleware<CatListState>
Store<AppState> Store<CatListState>
For sure this will be a problem when we
will try to build the main application
64. var store = Store<AppState>(reducer: appReducer,
state: nil,
middleware: [catListMiddleware])
Type of expression is ambiguous
without more context
Build the application: Middleware
65. f(x) = y
f: Middleware<CatListState> -> Middleware<AppState>
Build the application: Middleware
72. var store = Store<AppState>(reducer: appReducer,
state: nil,
middleware: [catListMiddleware])
Build the application: Middleware
73. var store = Store<AppState>(reducer: appReducer,
state: nil,
middleware: [pullback(input: catListMiddleware,
state: .catListState)])
Build the application: Middleware
74. @main
struct CatsApp: App {
var body: some Scene {
WindowGroup {
CatListView(store: store)
}
}
}
Cannot convert value of type 'Store<AppState>'
to expected argument type 'Store<CatListState>'
Build the application: Store
75. f(x) = y
f: Store<AppState> -> Store<CatListState>
Build the application: Store
76. func function(
typealias Reducer<State> =
(_ action: Action, _ state: State?) -> State
}
}
) -> Store<LocalState> {
Build the application: Store
extension Store {
77. func function(
typealias Reducer<State> =
(_ action: Action, _ state: State?) -> State
}
}
) -> Store<LocalState> {
Build the application: Store
extension Store {
state: KeyPath<State, LocalState>,
defaultState: LocalState
Store(reducer: { action, localState in
dispatch(action)
return self.state?[keyPath: state] ?? defaultState
}, state: self.state?[keyPath: state])
78. func function(
typealias Reducer<State> =
(_ action: Action, _ state: State?) -> State
}
}
) -> Store<LocalState> {
Build the application: Store
extension Store {
state: KeyPath<State, LocalState>,
defaultState: LocalState
Store(reducer: { action, localState in
dispatch(action)
return self.state?[keyPath: state] ?? defaultState
}, state: self.state?[keyPath: state])
84. Build the application: Store
LocalStore
LocalReducer
LocalView
Reducer
Action
Action
Updates
85. Build the application: Store
LocalStore
LocalReducer
LocalView
Store
Reducer
Action
Action Updates
Updates
86. Build the application: Store
LocalStore
LocalReducer
LocalView
Store
Reducer
Action
Action Updates No-one
listening
here
Updates
Updates
87. Build the application: Store
LocalStore
LocalReducer
LocalView
Store
Reducer
Action
Action Updates No-one
listening
here
Updates
Updates
Let’s change approach!
Create a wrapper instead!
88. Build the application: Store
class LocalStore {
let dispatchFunction: DispatchFunction
let subscribeFunction: (StoreSubscriber) -> Void
let unsubscribeFunction: (AnyStoreSubscriber) -> Void
let getState: () -> State
}
89. Build the application: Store
public var state: State {
getState() }
public func dispatch(_ action: Action) {
self.dispatchFunction(action) }
public func subscribe(_ subscriber: StoreSubscriber) {
subscribeFunction(subscriber) }
public func unsubscribe(_ subscriber: AnyStoreSubscriber) {
unsubscribeFunction(subscriber) }
90. func function(
typealias Reducer<State> =
(_ action: Action, _ state: State?) -> State
}
}
) -> LocalStore<LocalState> {
Build the application: Store
extension Store {
93. @main
struct CatsApp: App {
var body: some Scene {
WindowGroup {
CatListView(store: store.view(state: .catListState))
}
}
}
Build the application: Store
100. Summary
• ReSwift: implement Redux in Swift
• Modularization: a lot of bene
fi
ts
• Pullback to handle middlewares
101. Summary
• ReSwift: implement Redux in Swift
• Modularization: a lot of bene
fi
ts
• LocalStore wrapper implementation
• Pullback to handle middlewares
102. Summary
• ReSwift: implement Redux in Swift
• Modularization: a lot of bene
fi
ts
• LocalStore wrapper implementation
• Pullback to handle middlewares
• Store view function to assign local store to modules