Getting a Redux Vibe Into SwiftUI

Learn how to implement Redux concepts to manage the state of your SwiftUI app in a more predictable way by implementing a matching-pairs card game. By Andrew Tetlaw.

4.7 (19) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

Adding Middleware to the Store

Now, you'll add your middleware to your store. Add the following code to Store after the queue property in Store.swift:

private let middlewares: [Middleware<State, Action>]

Then, update init(initial:reducer:) so it matches the following:

init(
  initial: State,
  reducer: @escaping Reducer<State, Action>,
  middlewares: [Middleware<State, Action>] = []
) {
  self.state = initial
  self.reducer = reducer
  self.middlewares = middlewares
}

In AppMain.swift and ContentView.swift, replace the environmentObject modifier with the following:

.environmentObject(ThreeDucksStore(
  initial: ThreeDucksState(),
  reducer: threeDucksReducer,
  middlewares: [gameLogic]))

So, how do you call the middleware closure when dispatching an action? First, open Store.swift and add the following at the top of the file:

import Combine

Then, add the following code after middlewares property to save publisher subscriptions:

private var subscriptions: Set<AnyCancellable> = []

In your private dispatch method, add the following before the last line:

// 1
middlewares.forEach { middleware in
  // 2
  let publisher = middleware(newState, action)
  publisher
    // 3
    .receive(on: DispatchQueue.main)
    .sink(receiveValue: dispatch)
    .store(in: &subscriptions)
}

Here, you:

  1. Loop through all of the store's middlewares.
  2. Then, call the middleware closure to obtain the returned publisher.
  3. Make sure to receive the output on the main queue and send the actions to dispatch(_:).

Build and run your app!

Three Ducks un-flip cards

If you get a match, you'll see the flipped cards stay flipped. Unfortunately, you'll also notice that if they don't match, they unflip so fast you don't get to see the second card. That's a quick and easy fix.

In your gameLogic middleware where you return Just(.unFlipSelectedCards), add a delay for a second like this:

return Just(.unFlipSelectedCards)
  .delay(for: 1, scheduler: DispatchQueue.main)
  .eraseToAnyPublisher()

Build and run your app again. You should be able to flip all the cards once you find all the matches.

All cards are flipped

Winning the Game

The reveals are the next problem — how do you win the game? You should be used to this workflow by now! Add a new action to ThreeDucksAction in Actions.swift:

case winGame

Next, handle it with Reducer.swift by adding a winGame case to the switch statement:

case .winGame:
  mutatingState.gameState = .won

If you recall, when gameState is set to .won, it displays GameWinScreenView.

Now, you need to dispatch the action. Your gameLogic middleware is up to the job. At the top of the flipCard case statement, add the following:

// 1
let flippedCards = state.cards.filter { $0.isFlipped }
// 2
if flippedCards.count == state.cards.count {
  // 3
  return Just(.winGame)
    .delay(for: 1, scheduler: DispatchQueue.main)
    .eraseToAnyPublisher()
}

Here's what's going on:

  1. You create an array of all flipped cards by filtering cards.
  2. Then, check if flippedCards.count equals cards.count.
  3. If that's true, it's a win, and you return Just(.winGame).

Next, open GameWinScreenView.swift and add the environment variable for store before body:

@EnvironmentObject var store: ThreeDucksStore

Finally, add the following code in the action for the Go Again button:

store.dispatch(.endGame)

Build and run the app now. You should be able to match all the cards, see the winning screen and tap Go Again to return to the title screen.

Three Ducks game win screen

Where to Go From Here?

You can download the completed project using the Download Materials button at the top or bottom of this tutorial.

Well done! Your game is working great, and you've managed to get all your ducks in a row where your app state management code is concerned. No soggy bread held together by duct tape here!

If you're looking for an extra challenge, here are a few. You can also see a solution to each one in the final project if you need a hand.

Game Difficulty

On the title screen, there's a difficulty selector. Add a state value for the difficulty. Then, in your gameLogic middleware, implement a code that sets the initial card array based on the difficulty. Fewer cards for easy, more cards for hard.

High Score

Create a new middleware for storing the high score. Retrieve the high score on the app launch and display it on the title screen. When a game is won, check the score to see if it's a new high score.

Quack!

For a bit of fun, if the player flips a purple three ducks card, make the app play a duck quack sound. Implement a new middleware for this.

We hope you enjoyed this tutorial about adopting Redux concepts in SwiftUI. If you have any questions or comments, please join the forum discussion below.