How to Make a Game Like Wordle in SwiftUI: Part Two
Extend your Wordle word-game clone with animation, accessibility, statistics and shareable results, all in SwiftUI. By Bill Morefield.
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
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
How to Make a Game Like Wordle in SwiftUI: Part Two
35 mins
- Getting Started
- Showing the Player’s Result
- Colorizing the Keyboard
- Adding Animation
- Creating a Shake Animation
- Improving Accessibility
- Sharing Game Results
- Adding a Share Button
- Saving Game Results
- Displaying Statistics
- Building a Bar Chart
- Adding a Show Stats Button
- Remembering Where the Player Is
- Where to Go From Here?
In part one, you created models for the game elements and combined views based on these models into a functional Wordle clone called Guess The Word.
In part two, you’ll continue to add features and polish to the app by:
- Displaying the player’s results.
- Adding animation to the player’s guesses.
- Improving accessibility with feedback on the keyboard.
- Allowing the player to save and share their results, as well as displaying statistics.
Getting Started
You can pick up this tutorial from where you ended part one or access the starter project by clicking the Download Materials button at the top or bottom of this tutorial. Run the app, and you’ll find you can play the game and attempt to guess the word.
While the game works, it’s a bit unspectacular. The only indication that the player won comes in the line of green boxes, and a lost game provides no feedback. In the next section, you’ll give the player better feedback on their result.
Showing the Player’s Result
Open ContentView.swift and add the following new property after the game
property:
@State private var showResults = false
You’ll use this Boolean to trigger a view showing the final result when a game ends. Add the following code just before the frame(width:height:alignment:)
modifier:
.sheet(isPresented: $showResults) {
GameResultView(game: game)
}
This will show a property sheet with GameResultView(game:)
when showResults
becomes true. To set the property, add the following code immediately after what you just added:
// 1
.onChange(of: game.status) { newStatus in
// 2
if newStatus == .won || newStatus == .lost {
// 3
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
showResults = true
}
}
}
Here’s how this code handles the state change:
- The
onChange(of:perform:)
method executes the code inside the closure whengame.status
changes. The new value will be passed into the closure asnewStatus
. - If
newStatus
is eitherwon
orlost
, this means the game ended so it’s time to show the results. - Using
DispatchQueue.main.asyncAfter(deadline:qos:flags:execute:)
allows you to execute the code inside the method’s closure after a delay. Here, you wait 1.5 seconds before changing the state. This allows the player to see the result on the game board before showing the sheet.
Next, you’ll update the view the player sees when a game ends. Open GameResultView.swift in the GameBoardViews group and change the body
to:
VStack {
if game.status == .won {
Text("You got it!")
.font(.title)
.foregroundColor(.green)
} else {
Text("Sorry you didn't get the word in \(game.maxGuesses) guesses.")
.font(.title2)
.foregroundColor(.red)
}
Text("The word was \(game.targetWord).")
.font(.title2)
}
Also, update your preview to show both the win and loss states:
Group {
GameResultView(
game: GuessingGame.wonGame()
)
GameResultView(
game: GuessingGame.lostGame()
)
}
This view shows a congratulatory message if the player wins and a condolence message if they lose. Either way, the player sees the target word. Build and run the app and play a few games to see the new view in action. Remember the app shows the target word on the console when debugging.
Now that you have a better view telling the player their result, you’ll add some feedback to the player on the keyboard in the next section.
Colorizing the Keyboard
To set the background color of the keys to reflect the known status of letters, open GuessingGame.swift in the Models group and add the following code to the end of the class:
func statusForLetter(letter: String) -> LetterStatus {
// 1
if letter == "<" || letter == ">" {
return .unknown
}
// 2
let finishedGuesses = guesses.filter { $0.status == .complete }
// 3
let guessedLetters =
finishedGuesses.reduce([LetterStatus]()) { partialResult, guess in
// 4
let guessStatuses =
guess.word.filter { $0.letter == letter }.map { $0.status }
// 5
var currentStatuses = partialResult
currentStatuses.append(contentsOf: guessStatuses)
return currentStatuses
}
// 6
if guessedLetters.contains(.inPosition) {
return .inPosition
}
if guessedLetters.contains(.notInPosition) {
return .notInPosition
}
if guessedLetters.contains(.notInWord) {
return .notInWord
}
return .unknown
}
This method returns a letter’s status as follows:
For each pass, you filter any GuessedLetter
s for the letter in question. You then map
the status
property of any matching GuessedLetter
s into an array.
- The special keys always return
unknown
. - You only need to check completed guesses.
- You use the
reduce
method onfinishedGuesses
to build an array ofLetterStatus
enums for the letter. - The
reduce
method loops through the array with the current result inpartialResult
and the current letter from the array asguess
. - Since the
partialResult
passed to the closure is nonmutable, you create a copy and append the results from step four to it. You send this array to the next step. - Now
guessedLetters
contains an array with the status of the letter for any guess using the letter in question. You check if the array contains a status in order of preference. For example, if in one guess, E was in the correct position and in another guess, E is not in the correct position, the method will returninPosition
since you check for that first.
For each pass, you filter any GuessedLetter
s for the letter in question. You then map
the status
property of any matching GuessedLetter
s into an array.
Next, add the following code to the end of the class:
func colorForKey(key: String) -> Color {
let status = statusForLetter(letter: key)
switch status {
case .unknown:
return Color(UIColor.systemBackground)
case .inPosition:
return Color.green
case .notInPosition:
return Color.yellow
case .notInWord:
return Color.gray.opacity(0.67)
}
}
This returns a color for each status corresponding to those used in the GuessedLetter
struct. Open KeyboardView.swift in the KeyboardViews group and add the following code after the KeyButtonView
view:
.background(
game.colorForKey(key: key)
)
Build and run to see how your keyboard now reflects the status of each letter by color, helping the player prepare better guesses.
Next, you’ll jazz things up with some animation.
Adding Animation
When the player submits a guess, the letter status changes appear instantly. An animation would help draw the player’s attention and add a little drama to the reveal. Open GuessBoxView.swift and add the following code as the last modifier after cornerRadius(_:antialiased:)
:
// 1
.rotation3DEffect(
// 2
.degrees(letter.status == .unknown ? 0 : 180),
// 3
axis: (x: 0.0, y: 1.0, z: 0.0)
)
// 4
.animation(
.linear(duration: 1.0).delay(0.1 * Double(index)),
value: letter.status
)
This code will give the guessed letters a flip animation that cascades from left to right across the view. This is what the code does:
- You use
rotation3DEffect(_:axis:anchor:anchorZ:perspective:)
, a more flexible version of the more commonrotationEffect(_:anchor:)
modifier, letting you specify the axis of rotation. - The rotation angle depends on the letter’s status. The letter will rotate 180 degrees when its status changes to something other than
unknown
. - You specify a rotation axis using unit coordinates. This specifies the rotation should occur around the y axis running vertically down the center of the view.
- The animation modifier tells SwiftUI to animate any state change caused when
letter.status
changes. You use a linear animation that moves at constant speed between the two states and takes one second to complete. You also add a delay before the animation, which increases asindex
does. This delay makes the animation for the first guess occur before the second, which occurs before the third, etc.
Build and run the app, then enter a guess. Notice how the letters look backwards when the animation completes. The rotation affects the entire view, including the text. To fix the problem, add the following modifier directly after the Text
view, before any other modifiers:
.rotation3DEffect(
.degrees(letter.status == .unknown ? 0 : -180),
axis: (x: 0.0, y: 1.0, z: 0.0)
)
This initial rotation balances the one applied later so the letters appear to flip but end up facing the correct direction. Build and run to try it out.