Using Redux with MVVM on iOS

Using Redux with MVVM on iOS

Before we start, what is Redux? From redux.js.org:

Redux is a predictable state container for JavaScript apps.

So if you are not a web developer, it’s totally normal that you don’t know what Redux is. But it’s really easy to understand and the concept can be applied to everywhere, including iOS development.

Redux overview

To summarise, your app has a store. In this store, you have your application state. From your views, you send actions to the store and reducers execute these actions. As state is changed by reducers, store notifies the view so that it can reflect changes on UI.

Core benefits:

  • It encourages you to find the real state of your app and model every change and action. This makes everything easy to understand.
  • Maximises separation of concerns. Every component does only one job.
  • Improves testability. Reducers has pure functions that are really easy to test.
  • Store propagates state automatically. (So you don’t have to choose between delegation, blocks, notifications or KVO.)

However, Redux is very strict and not very easy to apply in iOS applications, mainly because of UIKit. I won’t go into details, but if you curious, I recommend you to watch Benjamin Encz’s great talk on this topic.

MVVM + Redux

MVVM is a lightweight, easy-to-adapt architecture, which is similar to MVC. However, it doesn’t really provide the benefits of Redux. So why not use some of the concepts? I will try to improve the communication between the view controller and view model using the principles from Redux.

For more on MVVM: Bohdan Orlov’s post on iOS Architecture Patterns

Example

Let’s implement a simple app, in which you list some movies in a table view and user can move them around, delete some or insert new ones.

First, define the state.

struct MoviesState {
   var movies: [Movie] = []
   var fetching: Bool = false
}

VM will be our store, so keep the state in VM.

class MoviesViewModel {
   private(set) var state = MoviesState()
}

In Redux, reducers mutate the state using pure functions. To keep it simple, we can define reducers as mutating functions on MoviesState. But they should be dead-simple, sync and atomic functions that does only one job.

extension MoviesState {
   mutating func reloadMovies(movies: [Movie]) {
      self.movies = movies
   }
}

reloadMovies function will just set movies array in state struct and when the state changes, we should notify view controller so that it can update UI to reflect changes.

But how?

  • VC can send an action to VM and update UI for current state, in case there’s any change. (BAD)
  • VC can assign itself as VM’s delegate and VM can inform delegate if there’s any change in state. Only problem is that VC cannot know which part of the state changed, (and taking the diff is hard) so it re-renders everything to be sure. (NOT THAT BAD)
  • VC can send an action to VM, and VM can inform VC with the exact change that happened on state. (GOOD)

To be able to that, we should also model changes that can happen in our state, and feed VC with these change objects so that it can sync UI with current state.

In our humble example, we can model changes with an enum like this:

extension MoviesState {
   enum Change {
      case none
      case fetchStateChanged
      case moviesChanged
   }
}

Then, all the mutating functions on state should return a Change object.

extension MoviesState {
   func reloadMovies(…) -> Change {
      ...
      return .moviesChanged
   }
}

Then, we need a way to propagate this change to VC. A simple handler block would do fine.

class MoviesViewModel {
   ...
   var stateChangeHandler: ((MoviesState.Change) -> Void)?
}

Finally in VC, all we need to do is:

class MoviesViewController {
   override func viewDidLoad() {
      super.viewDidLoad()
      model.stateChangeHandler = { change in
         switch change {
            case .none:
               break // no change
            case .moviesChanged:
               tableView.reloadData()
            case .fetchStateChanged:
               setLoadingViewVisible(model.fetching)
         }
      }
      model.fetchMovies()
   }
}

You might have noticed that fetchMovies is an async call, but it doesn’t have a callback. Because you don’t need it. That call will make some changes on state, over time. And every time it happens, stateChangeHandler block will be called with that exact change in state.

This is how fetchMovies call should look like:

func fetchMovies() {

   let fetchStateChange = state.setFetching(true)
   stateChangeHandler?(fetchStateChange)

   API.fetchMovies { movies in
      let reloadChange = state.reloadMovies(movies)
      stateChangeHandler?(reloadChange)
      let fetchStateChange = state.setFetching(false)
      stateChangeHandler?(fetchStateChange)
   }
}

We have used store, state and reducers from Redux. How about actions? Actually, we already used it. Since we don’t talk to MoviesState directly, all methods defined on VM can be seen as Redux actions that initiate a state change.

Bottom Line

  • We are using MVVM so it is still lightweight. No framework needed.
  • It becomes easy to keep VM and VC in sync. Even without binding frameworks.
  • We have explicit, fully testable state objects.
  • We have simple state propagation even for async calls.
  • UI update code in VC is not duplicated in multiple callbacks. State changes are very clear.


Thank you for reading! Please let me know what you think. All suggestions and questions are welcome.


Image Credit: Photo by Nick Fewings on Unsplash