Architecture
Services
All main services are stored in singleton manner in App
structure (App.swift
file). They are used by action and interactors.
Redux
At the hearth on the app lies Redux architecture. Architecture mechanism is a basically copy of ReSwiftopen in new window methods with some tweaks and without unneeded logic.
In app's implementation we have separate background queue that all Redux related calls are executed on (so developer needs to be awared of it while implementing business logic part - Redux calls are not on main thread).
The main app state is stored in AppState
structure that consists of inner sub-states. The same is done with app reducer (AppReducer.swift
file) - it handles all the updates using inner reducers. That's the way logical blocks of app are separated.
In Middlewares.swift
file all used middlewares can be found. Special attention should be paid to thunksMiddleware()
- basically it's the way the app uses to execute asynchronous calls using Redux actions. So we can separate actions by 2 types:
- common action (the one that is handled by reducer to update state based on action's parameters)
- thunk action (the one that uses thunks middleware to execute synchronous code and call other actions inside)
Example of common action:
struct MessagesActions {
struct UpsertItems: MessagesAction { // action handled by reducer
let items: [ChatMessage] // parameters that reducer have access to
}
}
2
3
4
5
6
Example of thunk action:
struct MessagesActions {
struct CacheItems { // action that executes code within its `thunk()` method, is able to call other actions
let items: [ChatMessage]
func thunk() -> Thunk<AppState> {
.init { dispatch, getState in
dispatch(UpsertItems(items: items)) // call to other common action
dispatch(EventsActions.UpsertMessageItems(messages: items).thunk()) // call to other thunk action
let localWorker = ChatMessageLocalWorker(coreDataStack: App.coreDataStack) // some additional logic
localWorker.upsert(entities: items)
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
VIPER
On the per-screen level app uses VIPER architecture in its common way. So each module consists of view, interactor, presenter, entity and router. There could be local module business logic in interactor.
There are two ways to connect VIPER layer with Redux layer:
- Each interactor can call Redux action whenever it's needed, for example:
func trackAvatarSelection() {
App.store.dispatch(GroupsActions.Analytics.ChangeGroupAvatar(profileType: group.accountType.profileType))
}
2
3
- Each interactor can subscribe on Redux's state change to make its module react to this change properly. It's done via
StoreSubscriber
protocol. Example:
extension EditGroupInfo.Interactor: EditGroupInfoInteracting {
// subsribe for changes
func subscribe() {
App.store.subscribe(self) { subscription in
subscription.only { [weak self] oldState, newState in
guard let self = self else {
return false
}
let hasSessionChanged = oldState.sessionState.userSession != newState.sessionState.userSession
let hasGroupChanged = oldState.conversationState.groupsState.items[self.groupID] !=
newState.conversationState.groupsState.items[self.groupID]
// here we're subscribing only for changes of userSession and specific group;
// other state changes will not fire subscription event for this interactor
return hasSessionChanged || hasGroupChanged
}
}
}
// unsubsribe from changes
func unsubscribe() {
App.store.unsubscribe(self)
}
}
// MARK: - StoreSubscriber
extension EditGroupInfo.Interactor: StoreSubscriber {
// this method will be called each time state we're subscribed for is changed;
// we have whole AppState as a parameter
func newState(state: AppState) {
output.didUpdateState(state) // call to presenter to initiate re-rendering cycle
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37