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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
ReSwift Tutorial: Memory Game App
35 mins
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:
-
AppState
now subscribes to the globalstore
. In the closure,select
indicates you are specifically subscribing to changes in theroutingState
. -
pushViewController
will be used to instantiate and push a given view controller onto the navigation stack. It usesinstantiateViewController
, which loads the view controller based on the passedidentifier
. - Make the
AppRouter
conform toStoreSubscriber
to getnewState
callbacks wheneverroutingState
changes. - You don’t want to animate the root view controller, so check if the current destination to push is the root.
- When the state changes, you push the new destination onto the
UINavigationController
using therawValue
ofstate.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:
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
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.
-
TableDataSource
is included in the starter and acts as a declarative data source forUITableView
. - Subscribe to the
menuState
onviewWillAppear
. Now you’ll get callbacks innewState
every timemenuState
changes. - Unsubscribe, when needed.
- 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.
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:
final class MenuTableViewController: UITableViewController {
var currentMenuTitlesState: [String]
...
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.
final class MenuTableViewController: UITableViewController {
var currentMenuTitlesState: [String]
...
Build and run, and you should now see the menu items:
Actions
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.