ReSwift Tutorial: Memory Game App

In this ReSwift tutorial, you’ll learn to create a Redux-like app architecture in Swift that leverages unidirectional data flow. By Michael Ciurus.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Subscribing

Remember that default menu value in RoutingState? That’s actually the current state of your app! You’re just not subscribing to it anywhere.

Any class can subscribe to the Store, not just Views. When a class subscribes to the Store, it gets informed of every change that happens in the current state or sub-state. You’ll want to do this on AppRouter so it can change the current screen in the UINavigationController when the routingState changes.

Open AppRouter.swift and replace AppRouter with the following:

final class AppRouter {
    
  let navigationController: UINavigationController
    
  init(window: UIWindow) {
    navigationController = UINavigationController()
    window.rootViewController = navigationController
    // 1
    store.subscribe(self) {
      $0.select {
        $0.routingState
      }
    }
  }
  
  // 2  
  fileprivate func pushViewController(identifier: String, animated: Bool) {
    let viewController = instantiateViewController(identifier: identifier)
    navigationController.pushViewController(viewController, animated: animated)
  }
    
  private func instantiateViewController(identifier: String) -> UIViewController {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    return storyboard.instantiateViewController(withIdentifier: identifier)
  }
}

// MARK: - StoreSubscriber
// 3
extension AppRouter: StoreSubscriber {
  func newState(state: RoutingState) {
    // 4
    let shouldAnimate = navigationController.topViewController != nil
    // 5
    pushViewController(identifier: state.navigationState.rawValue, animated: shouldAnimate)
  }
}

In the code above, you updated AppRouter and added an extension. Here’s a closer look at what this does:

  1. AppState now subscribes to the global store. In the closure, select indicates you are specifically subscribing to changes in the routingState.
  2. pushViewController will be used to instantiate and push a given view controller onto the navigation stack. It uses instantiateViewController, which loads the view controller based on the passed identifier.
  3. Make the AppRouter conform to StoreSubscriber to get newState callbacks whenever routingState changes.
  4. You don’t want to animate the root view controller, so check if the current destination to push is the root.
  5. When the state changes, you push the new destination onto the UINavigationController using the rawValue of state.navigationState, which is the name of the view controller.

AppRouter will now react to the initial menu value and push the MenuTableViewController on the navigation controller.

Build and run the app to check it out:

ReSwift tutorial

Your app displays MenuTableViewController, which is empty. You’ll populate it with menu options that will route to other screens in the next section.

The View

ReSwift tutorial

Anything can be a StoreSubscriber, but most of the time it will be a view reacting to state changes. Your objective is to make MenuTableViewController show two different menu options. It’s time for your State/Reducer routine! 

Go to MenuState.swift and create a state for the menu with the following:

import ReSwift

struct MenuState: StateType {
  var menuTitles: [String]
  
  init() {
    menuTitles = ["New Game", "Choose Category"]
  }
}

MenuState consists of menuTitles, which you initialize with titles to be displayed in the table view.

In MenuReducer.swift, create a Reducer for this state with the following code:

import ReSwift

func menuReducer(action: Action, state: MenuState?) -> MenuState {
  return MenuState()
}

Because MenuState is static, you don’t need to worry about handling state changes. So this simply returns a new MenuState.

Back in AppState.swift, add MenuState to the bottom of AppState.

let menuState: MenuState

It won’t compile because you’ve modified the default initializer once again. In AppReducer.swift, modify the AppState initializer as follows:

return AppState(
  routingState: routingReducer(action: action, state: state?.routingState),
  menuState: menuReducer(action: action, state: state?.menuState))

Now that you have the MenuState, it’s time to subscribe to it and use it when rendering the menu view.

Open MenuTableViewController.swift and replace the placeholder code with the following:

import ReSwift

final class MenuTableViewController: UITableViewController {
  
  // 1
  var tableDataSource: TableDataSource<UITableViewCell, String>?
  
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // 2
    store.subscribe(self) {
      $0.select {
        $0.menuState
      }
    }
  }
  
  override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    // 3
    store.unsubscribe(self)
  }
}

// MARK: - StoreSubscriber
extension MenuTableViewController: StoreSubscriber {
  
  func newState(state: MenuState) {
    // 4
    tableDataSource = TableDataSource(cellIdentifier:"TitleCell", models: state.menuTitles) {cell, model in
      cell.textLabel?.text = model
      cell.textLabel?.textAlignment = .center
      return cell
    }
    
    tableView.dataSource = tableDataSource
    tableView.reloadData()
  }
}

The controller now subscribes to MenuState changes and renders the state in the UI declaratively.

  1. TableDataSource is included in the starter and acts as a declarative data source for UITableView.
  2. Subscribe to the menuState on viewWillAppear. Now you’ll get callbacks in newState every time menuState changes.
  3. Unsubscribe, when needed.
  4. This is the declarative part. It’s where you populate the UITableView. You can clearly see in code how state is transformed into view.

The newState callback defined in StoreSubscriber passes state changes. You might be tempted to capture the value of the state in a property, like this:

But writing declarative UI code that clearly shows how state is transformed into view is cleaner and much easier to follow. The problem in this example is that UITableView doesn’t have a declarative API. That’s why I created TableDataSource to bridge the gap. If you’re interested in the details, take a look at TableDataSource.swift.

Note: As you might have noticed, ReSwift favors immutability – heavily using structures (values) not objects. It also encourages you to create declarative UI code. Why?
final class MenuTableViewController: UITableViewController {
  var currentMenuTitlesState: [String]
  ...
final class MenuTableViewController: UITableViewController {
  var currentMenuTitlesState: [String]
  ...

Build and run, and you should now see the menu items:

ReSwift tutorial

Actions

ReSwift tutorial

Now that you have menu items, it would be awesome if they opened new screens. It’s time to write your first Action.

Actions initiate a change in the Store. An Action is a simple structure that can contain variables: the Action’s parameters. A Reducer handles a dispatched Action and changes the state of the app depending on the type of the action and its parameters.

Create an action in RoutingAction.swift:

import ReSwift

struct RoutingAction: Action {
  let destination: RoutingDestination
}

RoutingAction changes the current routing destination.

Now you’re going to dispatch RoutingAction when a menu item gets selected.

Open MenuTableViewController.swift and add the following in MenuTableViewController:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  var routeDestination: RoutingDestination = .categories
  switch(indexPath.row) {
  case 0: routeDestination = .game
  case 1: routeDestination = .categories
  default: break
  }
  
  store.dispatch(RoutingAction(destination: routeDestination))
}

This sets routeDestination based on the row selected. It then uses dispatch to pass the RoutingAction to the Store.

The Action is getting dispatched, but it’s not supported by any reducer yet. Go to RoutingReducer.swift and replace the contents of routingReducer with the following code that updates the state:

var state = state ?? RoutingState()

switch action {
case let routingAction as RoutingAction:
  state.navigationState = routingAction.destination
default: break
}

return state

The switch checks if the passed action is a RoutingAction. If so, it uses its destination to change the RoutingState, then returns it.

Build and run. Now when you tap on menu items, the corresponding view controllers will be pushed on top of the navigation controller.

ReSwift tutorial

Michael Ciurus

Contributors

Michael Ciurus

Author

Jeff Rames

Tech Editor

Chris Belanger

Editor

Andy Obusek

Final Pass Editor and Team Lead

Over 300 content creators. Join our team.