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.
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
Getting a Redux Vibe Into SwiftUI
30 mins
- Getting Started
- Exploring Three Ducks
- SwiftUI, Meet Redux
- Creating the App State
- Creating the Store
- Observing State Changes
- Creating Actions
- Making a Reducer
- Dispatching Actions
- Putting Your Reducer to Work
- Vibing out to Redux
- Flipping the Cards
- Adding Your First Middleware
- Unflipping Cards
- Adding Middleware to the Store
- Winning the Game
- Where to Go From Here?
- Game Difficulty
- High Score
- Quack!
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:
- Loop through all of the store's middlewares.
- Then, call the middleware closure to obtain the returned publisher.
- Make sure to receive the output on the main queue and send the actions to
dispatch(_:)
.
Build and run your app!
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.
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:
- You create an array of all flipped cards by filtering
cards
. - Then, check if
flippedCards.count
equalscards.count
. - 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.
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.