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
    }
}
1
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)
            }
        }
    }
}
1
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))
}
1
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
    }
}
1
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