Things We Learned from ReSwift

Arif Fikri
iOS Developer

ReSwift is a framework to help developers make the application state manageable. ReSwift is a representative of Redux in the Swift world.

How is it done? By splitting up app components so that they communicate with each other in a single direction. Introducing a unidirectional flow of how the state changes and having it in a single place makes components do not need to keep track of state in the form of variables, which reduces complexity and bugs due to concurrent changes.

The framework has helped our engineers orchestrate the app components and debug issues. But to our experience, there are multiple ways one can abuse it. And we would love to share our experience in approaching it. In particular, the anti-patterns we encountered and gave us trouble down the line.

Accessing the State Directly

ReSwift encourages you to embrace the unidirectional data flow. This means the view layer is notified every time there is a state change so it can react accordingly. Here is one example where we have done it wrong:

AppState.swift

struct AppState {
    let counter: Int
}

CounterViewController.swift

func displayCounter() {
    let currentCount = appStore.state.counter
    // …display the count
}

The ReSwift documentation explains that a new state is passed by the Store to its subscribers whenever available, this is where we can adjust our view to reflect the latest changes. Accessing the state directly breaks this pattern, because we should let ReSwift orchestrate the changes required.

Here is a better way to read the value of the property in the state.

AppState.swift

struct AppState {
    let counter: Int
}

CounterViewController.swift

func newState(state: AppState) {
    updateCounter(count: state.counter)
}

func updateCounter(count: Int) {
  // ...display the count
}

By only accessing the state in the newState(state:) call we ensure to always be reading up-to-date values. As the result of that, all our controllers will update their views upon any state change.

Using actions as a way to show another View Controller

This happened quite unnoticed and we did not realise it would have been a problem at first.

MainViewController.swift

store.dispatch(MenuOpenAction())

MenuViewController.swift

newState(state: AppState) {
    switch state.lastAction {
        case is MenuOpenAction:
            openMenu()
    }
}

The snippet above demonstrates that the MenuOpenAction dispatch is used to trigger the openMenu method in MenuViewController.swift. There is no relationship with the app state at all. There are two ways to make this better. 1) To use another approach such as delegate method 2) Introduce a property to store the state of the menu.

MainViewController.swift

store.dispatch(MenuAction(isOpen: true))

MenuViewController.swift

newState(state: AppState) {
    if state.menuState.isOpen != isCurrentMenuOpen {
        openMenu(state.menuState.isOpen)
    }
}

This way we are using the action to acknowledge the menu state change, then reading the isOpen property to invoke code that reflects the change.

Storing UI components in AppState

There were also moments when we used app state to store UI components. This is, without a doubt, a big mistake. UI components like UILabel, UITextView, or UIButton are not state, therefore not something we should consider to be part of state.

AppState.swift

struct AppState {
    var playPauseButton: UIButton
}

PlayerViewController.swift

func changePlayerButton() {
    appState.playPauseButton.image = UIImage(named: "pause.png")
}

The reason we ended up with this is that we were using the availability of the app state to access and modify the button anywhere, without needing direct access to the UIViewController where the button was. However this is incorrect. The button is a UI component, it may not exist at some moment in the app lifecycle, such that, it might be hidden, or the view controller might have been dismissed from the stack. At this point, the property would be a nil pointer.

AppState.swift

struct AppState {
    enum ButtonState {
        case play
        case pause
        case stop
    }
    var playButtonState: ButtonState
}

PlayerViewController.swift

func newState(state: AppState)
    if state.playButtonState == pause {
        playPauseButton.image = UIImage(named: pause.png)
    }
}

Conclusion

We have been using the library for almost 2 years now, there are mix opinions by the team around it. As in any framework that exists, there is no single solution that is a perfect fit for your project. If your team is also considering it, there are many concerns to have a look on to make sure you make the most out of the framework.