Redux Architecture in iOS Applications
About ten years ago, a bug emerged from Facebook’s messenger app where the unread count was not same on each views. Although there is only one unread count for an application, each view had different number. This was looking weird and not acceptable. In addition, it was hard to debug. Facebook had to find a absolute solution for that. And its developers decided to implement a unidirectional pattern which is Flux. Flux is a design pattern, in 2015, Dan Abramov and Andrew Clark invented Redux as a JavaScript implementation and Redux architecture was born.
What is Redux?
Redux is an architecture that commonly used in web application development with React. It has three fundamental principles.
Single source of truth: A single store has the all state of the whole application.
State is read-only: The only way to update the store is dispatching a regarding action.
Changes are made with pure functions: Reducers are pure functions that takes the old store and an action as parameter, and returns new state based on them.
Redux in iOS
For React applications, the most common way to integrate Redux architecture is to use React-Redux library. Redux is not a prevalent approach to develop an iOS application. However, there are some current libraries for Swift. Implementing a library for Redux does not require comprehensive work. Let’s create a simple Redux Library.
First, we need to implement a Store class. This class has a state variable, a reducer to update the state and a subscriber list to notify views to update themselves.
Store
public class Store<StateType: State> {
private let reducer: Reducer<StateType>
private var state: StateType?
private var subscribers: [AnyStoreSubscriber] = []
public init(reducer: @escaping Reducer<StateType>, state: StateType?) {
self.reducer = reducer
self.state = state
}
public func dispatch(_ action: Action) {
state = reducer(action, state)
subscribers.forEach { $0._newState(state: state!) }
}
public func subscribe(_ newSubscriber: AnyStoreSubscriber) {
subscribers.append(newSubscriber)
subscribers.forEach { $0._newState(state: state!) }
}
public func unsubscribe(_ subscriber: AnyStoreSubscriber) {
for (index, value ) in subscribers.enumerated() where value === subscriber {
if index < subscribers.count {
self.subscribers.remove(at: index)
}
}
}
The state is a struct that contains all the view model element of entire application. For instance, if it is a chat application, we need to put all messages, conversations, contact numbers etc. into the state. While implementing the library, we need to use generic parameters on our side and the real implementation of the state should be on application side.
Reducer
public typealias Reducer<StateType: State> = (_ action: Action, _ state: StateType?) -> StateType
The reducer only a function that takes an action and state as parameter and return new state.
StoreSubscriber
public protocol StoreSubscriber: AnyStoreSubscriber {
associatedtype StateType
func newState(state: StateType)
}
We need to create a StoreSubscriber protocol whom viewcontrollers conform and update themselves when newState function is triggered.
To-Do Example
We have created generic elements of Redux architecture in last section. Now, we need to create To-Do specific ones.
Let’s remind the principle; user dispatch actions to store and store updates its state via reducer, then notify views to update themselves.
Actions
Create actions as struct which conforms SwiftRedux Action. We need to think of all the action that user may done. In our To-Do example, we have only three different actions.
import SwiftRedux
struct AddToDoAction: Action {
let title: String
let desc: String
var toDoToAdd: ToDo {
return ToDo(title: title, desc: desc)
}
}
struct RemoveToDoAction: Action {
let id: String
}
struct UpdateToDoAction: Action {
let id: String
let newTitle: String
let newDesc: String
var updatedToDo: ToDo {
return ToDo(id: id, title: newTitle, desc: newDesc)
}
}
Reducer
First, let’s create an AppReducer to handle store update. This function takes an action and current state as parameter, then generates new state and return.
import SwiftRedux
func AppReducer(action: Action, state: AppState?) -> AppState {
return AppState(
todos: toDoReducer(action: action, todos: state?.todos)
)
}
func toDoReducer(action: Action, todos: [ToDo]?) -> [ToDo] {
var todos = todos ?? []
switch action {
case let addAction as AddToDoAction:
todos.append(addAction.toDoToAdd)
case let removeAction as RemoveToDoAction:
todos.removeAll(where: {
$0.id == removeAction.id
})
case let updateAction as UpdateToDoAction:
todos.removeAll(where: {
$0.id == updateAction.id
})
todos.append(updateAction.updatedToDo)
default:
break
}
return todos
}
State
Because this is a simple To-Do app, our state contains only a todo list.
struct AppState: State {
var todos: [ToDo] = []
}
Store
We need to create a single store somewhere global. Here, a gloabal App singleton created and the store constructed inside.
import SwiftRedux
let app: App = App()
final class App: NSObject {
let mainStore = Store<AppState>(
reducer: AppReducer,
state: App.initialState
)
// Has to be changed with db result some similar thing in real implementation
private static var initialState: AppState {
var state = AppState()
state.todos = [
ToDo.dummy(index: 1),
ToDo.dummy(index: 2),
ToDo.dummy(index: 3),
ToDo.dummy(index: 4),
ToDo.dummy(index: 5),
ToDo.dummy(index: 6),
ToDo.dummy(index: 7),
ToDo.dummy(index: 8),
ToDo.dummy(index: 9)
]
return state
}
}
Usage on App side
Let’s create a ToDoListVC to display all ToDo entities in Store. The lifecycle functions can be used to handle subscription.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
app.mainStore.subscribe(self)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
app.mainStore.unsubscribe(self)
}
When the ToDoListVC is subscribed, the store triggers the newState function and ToDoListVC can construct its view.
extension ToDoListVC: StoreSubscriber {
typealias StateType = AppState
func newState(state: AppState) {
todos = state.todos
todoListView?.reload()
}
}
Let’s imagine, user tries to delete an ToDo. The user swipes the tableViewCell and pressed on delete button. Here, we need to dispatch an action which is a RemoveToDoAction.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
app.mainStore.dispatch(RemoveToDoAction(id: todos[indexPath.row].id))
}
}
When, the RemoveToDoAction is dispatched, the AppReducer interprets the action and returns a new state, then, the store notify all subscribers with new state, and subscribers updates their UI with new state in newState function.
Conclusion
Even if we can not say, Redux is the best approach to develop and iOS app, it looks useful in some aspects. Especially, it might be useful for small apps. However, we do not know what is the best way to create app via SwiftUI and Combine yet. Maybe, a variation of Redux would be the best way to develop applications with them in the future.