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.

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

Remembering Where the Player Is

There’s a rather annoying bug in the game right now. Run the app and make a couple of guesses.

Now force quit. When you restart the game, you’ll lose your progress. Whenever the app gets stopped or cleared from memory, the player loses their current game. To fix that, you’ll need a way to store the game’s current state and know when to load and save that state. Open GuessingGame.swift and add the new property:

@AppStorage("GameState") var gameState = ""

Again you’ll use the @AppStorage property wrapper to store information in UserDefaults. Next, add the following method to the end of the class:

func saveState() {
  let guessList = 
    guesses.map { $0.status == .complete ? "\($0.letters)>" : $0.letters }
  let guessedKeys = guessList.joined()
  gameState = "\(targetWord)|\(guessedKeys)"
  print("Saving current game state: \(gameState)")
}

This uses the map method on the guesses array. Each guess will become a string with the letters in the guess plus a > for complete guesses. You join the elements of the string array into a single string. You then store a string consisting of the targetWord, a | separator, and then the combined string that represents the current guesses. Next, you’ll add a method to bring this game state back in.

Add the following method to the end of the class:

func loadState() {
  // 1
  print("Loading game state: \(gameState)")
  currentGuess = 0
  guesses = .init()
  guesses.append(Guess())
  status = .inprogress

  // 2
  let stateParts = gameState.split(separator: "|")
  // 3
  targetWord = String(stateParts[0])
  // 4
  guard stateParts.count > 1 else { return }
  let guessList = String(stateParts[1])
  // 5
  let letters = Array(guessList)
  for letter in letters {
    let newGuess = String(letter)
    addKey(letter: newGuess)
  }
}

Loading the state is a bit more complicated but just reverses the steps:

  1. First you reset the game by setting the current guess to zero, clearing the guesses array before adding an empty guess and then setting the status to inprogress.
  2. Next, you take the gameState string and split it at the | character to get an array of two strings.
  3. You set the target word to the first string of this array.
  4. You ensure there is a second string, which might not be the case if the player began a game, but didn’t enter any letters. If there are letters, you store them in guessList.
  5. You now convert the guessList into an array of characters and then loop through each letter. You convert each letter to a String and then call addKey(letter:) with that letter. The result rebuilds the game state in the same way as if the player tapped the letters directly.

One more thing in this class; any time the game completes, you want to clear any saved state.

At the top of the class, find the declaration of the status property and replace it with:

var status: GameState = .initializing {
  didSet {
    if status == .lost || status == .won {
      gameState = ""
    }
  }
}

Whenever the status property changes, you use the didSet property observer to check if the new status is lost or won. If so, you set the gameState to an empty string since the game completed and no longer needs to be restored.

Now, you need to update the app to store and load the game when needed. Open ContentView.swift and add a new property at the top:

@Environment(\.scenePhase) var scenePhase

The scenePhase environment variable indicates a scene’s operational state. By monitoring changes, you can tell when you need to load or save the game state.

Add the following code after the existing use of onChange(of:perform:):

// 1
.onChange(of: scenePhase) { newPhase in
  // 2
  if newPhase == .active {
    if game.status == .new && !game.gameState.isEmpty {
      game.loadState()
    }
  }
  // 3
  if newPhase == .background || newPhase == .inactive {
    game.saveState()
  }
}

This code manages the state by:

  1. Monitoring changes in the scenePhase property. The new state will be passed into the closure as newPhase.
  2. If the app becomes active, you check if the status of the game is new, meaning no player action has taken place and the gameState property isn’t empty. The presence of stored state implies there’s a game to restore and is why you clear the state when a game ends. If both are true, then tell the game to load the saved state. In the case where the app hadn’t been pushed from memory, the status of the game will be inprogress so it retains the player’s current efforts.
  3. Any time the app goes into the background state, you tell the game to store its current state in case it’s needed later.

Build and run the app, start a game and then force quit. You should see the console message telling you the app saved the state. Start the app again and the game will pick up where you left off.

Animation showing player's state is now saved

Where to Go From Here?

You can use the Download Materials button at the top and bottom of this tutorial to download the starter and final projects. You’ve built a nice Wordle clone in SwiftUI and, in the process, learned about modeling the game, building an app and adding finishing touches to improve the player’s experience.

Don’t forget to read part one of this tutorial on building this app.

For more about accessibility, see the iOS Accessibility in SwiftUI Tutorial series.

For more on animation in SwiftUI, see Chapter 19 of SwiftUI by Tutorials.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!