Redux Architecture in iOS Applications

Mertkutluca
Accenture Industry X Turkey
5 min readMar 13, 2023

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.

Links to Project

Resources

--

--