dely Tech Blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

Redux for iOS apps

I'm Ishida, iOS engineer in TRILL. In this article I introduce Redux architecture and implementation for iOS.

TRILL engineering team has 13 engineers, about half of workers are global engineers (April 2022). We hold Study Meetup every week and a speaker talks about something technical (for example, docker, AWS, scrum, etc.) in English.

I talked about Redux architecture there, so I also introduce that in this article. I introduce basic and abstract features because the meetup has various engineers (iOS, Android, backend, etc).

delyではグローバル採用を進めており、日本人に限らず外国籍の方も積極的に採用しています。 もちろん全員が英語が堪能ということはなく、バイリンガルのメンバーや翻訳ツール、グローバルチーム (通訳できるメンバーが在籍) の力を借りながら進めています。 週1で行っている勉強会で発表した内容をこの記事にまとめたので、なんとなくTRILLのグローバル化を感じてもらえればと思います。 折角なので日本語で書いて英語に自動翻訳、ということはせず英語で書きました (とはいえ多少調べました…) 。

Background

In this decade, iOS application become bigger and architecture is more important. We have many architectures for iOS app, MVC, MVP, MVVM, Clean architecture, VIPER, etc.

This article introduces Redux architecture.

About Redux

Redux architecture is from web front-end technology.

Originally, web front-end was simple and clear because it didn't have state and API connection was simple. Recently web front-end has become bigger and more complex, so we've needed architecture against that.

In the situation, Redux architecture was created. And Redux try to solve those problems by two concepts: mutation and asynchronicity.

redux.js.org says "Redux can be described in three fundamental principles". Three fundamental principles are:

  • Single source of truth
  • State is read-only
  • Changes are made with pure functions

Roles and data flow of Redux is like below figure.

reference source

Redux has 4 roles: action, state, reducer, store.

The app has one store that has states and reducers. UI dispatches an action, and the reducer receives action and state. The state is updated, the store notifies the UI that.

In next section, I introduce the implementation by ReSwift.

Implementation

I implemented a simple counter app. This app has 2 tabs, one tab (counter tab) has counter label and button, the other tab (result tab) has counter label. When I tap a button in counter tab, 2 label in each 2 tabs change.

App's screenshots are below:

CounterViewController ResultViewController

CounterViewController has 1 label and 1 button, and ResultViewController has 1 label.

First we define each roles, action, state, reducer, store. Each roles are defined below.

struct AppState {
    var counter: Int = 0
}

struct CounterActionIncrease: Action {}

func counterReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState()

    switch action {
    case _ as CounterActionIncrease:
        state.counter += 1
    default: break
    }

    return state
}

let mainStore = Store<AppState>(
    reducer: counterReducer,
    state: nil
)

In this app, state has only one parameter (counter). Store has a reducer and the reducer receives action and state. Action detail is described in reducer (increment counter).

Next CounterViewController is below.

final class CounterViewController: UIViewController, StoreSubscriber {
    @IBOutlet private weak var counterLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        mainStore.subscribe(self)
    }

    func newState(state: AppState) {
        counterLabel.text = "\(state.counter)"
    }

    @IBAction func increment(_ sender: UIButton) {
        mainStore.dispatch(CounterActionIncrease())
    }
}

For simplicity, this view controller subscribes in viewDidLoad() When users tap increment button, increment(_ sender:) function is called. This function sends CounterActionIncrease() to dispatch of mainStore. This is just Redux data flow.

When state changes, this class receives in newState(state:) function. And this function sets new data to counterLabel.

ResultViewController is below.

final class ResultViewController: UIViewController, StoreSubscriber {
    @IBOutlet private weak var resultLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        mainStore.subscribe(self)
    }

    func newState(state: AppState) {
        resultLabel.text = "\(state.counter)"
    }
}

ResultViewController just subscribes state. When state changes, newState(state:) function is called and set to label. The label changes without manual update.

This app is too simple, so we can implement without Redux architecture. But this architecture makes easier to read codes by defining some roles.

Conclusion

I introduced Redux architecture. Apple announced SwiftUI and iOS app architectures have changed recently. I'd like to catch up architectures in the future.

We're hiring engineers and designers. If you are interested in our company, please contact us.

dely.jp