Chapters

Hide chapters

macOS Apprentice

First Edition · macOS 13 · Swift 5.7 · Xcode 14.2

Section II: Building With SwiftUI

Section 2: 6 chapters
Show chapters Hide chapters

Section III: Building With AppKit

Section 3: 6 chapters
Show chapters Hide chapters

6. Getting Data Into Your App
Written by Sarah Reichelt

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the previous chapter, you built the user interface for the game view of your Snowman game. The displayed data was hard-coded into the app and users couldn’t do anything to change it.

Now, you’ll learn how to create data types for use by SwiftUI. You’ll see how SwiftUI passes data around the app, and how it keeps the data and the user interface in sync.

You’ll also encounter property wrappers, which are a way of giving properties super-powers. SwiftUI uses these extensively.

Designing the Data Model

Start by opening your project from the last chapter, or use the starter project from the downloaded materials for this chapter.

Run the app to remind yourself of the layout and what data it needs:

The starter project
The starter project

Looking at the game view, here’s what the app needs to know:

  • How many incorrect guesses the player has made.
  • What text to show in the status area.
  • The secret word.
  • The player’s guesses.
  • Whether the player has won, lost or the game is still in progress.

You’ll add all this to a new structure called Game.

In the Project navigator, select SnowmanApp.swift and choose File ▸ New ▸ File… or press Command-N to create a new file. This time, choose macOS ▸ Source ▸ Swift File. Click Next and call the file Game.swift. Then, click Create to save it.

Start the new structure by adding this under the import line:

// 1
struct Game {
  // 2
  var incorrectGuessCount = 0
  var statusText = "Enter a letter to start the game."
  var word = "SNOWMAN"
  var guesses: [String] = []
}

This is similar to code you wrote in Section 1:

  1. You define a structure with the keyword struct followed by its name.
  2. Inside the structure, you declare properties to cover most of the required data. These are all initialized to default starting values.

This doesn’t cover the state of the game. Since there are three possible states, this is a great place to use an enumeration.

Adding an Enumeration

Press Command-N to create another Swift file and call it GameStatus.swift.

// 1
enum GameStatus {
  // 2
  case won
  case lost
  case inProgress
}
var gameStatus = GameStatus.inProgress
@State var game = Game()

Property Wrappers

When you see a property definition preceded by a word starting with @, you know that this is a property wrapper. Property wrappers are ways of enhancing properties to give them extra functionality.

Using the Data Model

Now you have your game property, you can start replacing the static elements in your UI with real data.

Image("\(game.incorrectGuessCount)")
Text(game.statusText)
Updated preview
Urfemus qvuhiun

Identifying the Letters

You use a ForEach loop to display the letters, and this requires that each element in the loop have an identifier. As a temporary measure, you used the letter itself as its identifier.

// 1
import SwiftUI

// 2
struct Letter: Identifiable {
  // 3
  let id: Int
  // 4
  let char: String
  var color = Color.blue
}
// 1
var letters: [Letter] {
  // 2
  var lettersArray: [Letter] = []

  // 3
  for (index, char) in word.enumerated() {
    // 4
    let charString = String(char)
    // 5
    if guesses.contains(charString) {
      let letter = Letter(id: index, char: charString)
      lettersArray.append(letter)
    } else if gameStatus == .lost {
      // 6
      let letter = Letter(id: index, char: charString, color: .red)
      lettersArray.append(letter)
    } else {
      // 7
      let letter = Letter(id: index, char: "")
      lettersArray.append(letter)
    }
  }
  // 8
  return lettersArray
}

Displaying the Letters

In the previous chapter, you separated LettersView into its own file. Open LettersView.swift and unpin the GameView preview, so that you’re seeing the preview for this file only.

let letters: [Letter]
ForEach(letters) { letter in
Text(letter.char)
LettersView(letters: Game().letters)
LettersView(letters: game.letters)
Previewing some letters.
Vbaliaqowv tide lodqebl.

.foregroundColor(letter.color)
Previewing a lost game.
Tcoyoakirr u royg zeyu.

var guesses: [String] = []
var gameStatus = GameStatus.inProgress

Styling the Button

Working down through GameView, the next item is the New Game button. It should only appear when the game is over.

// 1
if game.gameStatus != .inProgress {
  // 2
  Button("New Game") {
    print("Starting new game.")
  }
  .keyboardShortcut(.defaultAction)
}
.opacity(game.gameStatus == .inProgress ? 0 : 1)
mood = isRaining ? "sad" : "happy"

Searching for Modifiers

When working in SwiftUI, you’ll often think that there must be a modifier for some situation, but you don’t know what it’s called. Use Xcode’s Library to help you find it.

Using the Xcode library.
Eleff kzi Fnomu favwajw.

.disabled(true)
.disabled(game.gameStatus == .inProgress)

The Guesses View

The remaining section of the UI to connect is GuessesView. The first part you’ll add is the text entry field where players enters their guesses.

@State var nextGuess = ""
TextField("", text: $nextGuess)
// 1
.frame(width: 50)
// 2
.textFieldStyle(.roundedBorder)
// 3
.disabled(game.gameStatus != .inProgress)

Sending the Game Data

This view needs data from the game, but it also has to edit the game when the player makes a guess.

@Binding var game: Game
GuessesView(game: .constant(Game()))
GuessesView(game: $game)
Text(game.guesses.joined(separator: ", "))
Running with live data.
Qadxuxk boxt jawo cuke.

Playing the Game

It all looks good, so now it’s time to start playing the game. The first step is to process what the player types in the text field.

// 1
mutating func processGuess(letter: String) {
  // 2
  guard
    // 3
    let newGuess = letter.first?.uppercased(),
    // 4
    newGuess >= "A" && newGuess <= "Z",
    // 5
    !guesses.contains(newGuess)
  else {
    return
  }

  // 6
  if !word.contains(newGuess) && incorrectGuessCount < 7 {
    incorrectGuessCount += 1
  }
  guesses.append(newGuess)

  // 7
  // checkForGameOver()
}
// 1
.onChange(of: nextGuess) { newValue in
  // 2
  game.processGuess(letter: newValue)
  // 3
  nextGuess = ""
}
Entering guesses
Ijfovows doedmew

Ending the Game

The last step is to work out whether the player has won or lost the game.

// 1
mutating func checkForGameOver() {
  // 2
  let unmatchedLetters = word.filter { letter in
    !guesses.contains(String(letter))
  }

  // 3
  if unmatchedLetters.isEmpty {
    gameStatus = .won
    statusText = "HURRAY!!!! YOU WON!"
  } else if incorrectGuessCount == 7 {
    // 4
    gameStatus = .lost
    statusText = "You lost. Better luck next time."
  } else {
    // 5
    statusText = "Enter another letter to guess the word."
  }
}
Losing a game.
Juqogt e mopu.

Starting a New Game

You’ll start with choosing a random word. In the assets folder downloaded for this chapter, there’s a file called words.txt that lists over 30,000 words. Drag this file from assets into the Models group in the Project navigator.

Adding the words file.
Enhadw fne kubkn goca.

// 1
func getRandomWord() -> String {
  // 2
  guard
    // 3
    let url = Bundle.main.url(forResource: "words", withExtension: "txt"),
    // 4
    let wordsList = try? String(contentsOf: url) 
  else {
    // 5
    return "SNOWMAN"
  }

  // 6
  let words = wordsList.components(separatedBy: .newlines)

  // 7
  let word = words.randomElement() ?? "SNOWMAN"

  // 8
  print(word)
  return word.uppercased()
}
init() {
  word = getRandomWord()
}
game = Game()
Winning a game
Boxyiww i yane

Tweaking the App

Playing the game, there are a few improvements to make.

@FocusState var entryFieldHasFocus: Bool
.focused($entryFieldHasFocus)
.onChange(of: game.gameStatus) { _ in
  entryFieldHasFocus = true
}
Setting focus
Sajbobd vadac

.frame(minWidth: 1100, minHeight: 500)
let words = wordsList.components(separatedBy: .newlines)
// 1
let words = wordsList
  .components(separatedBy: .newlines)
  // 2
  .filter { word in
    // 3
    word.count >= 4 && word.count <= 10
  }
Tweaked GameView
Kjuacej LaboDoeq

Key Points

  • SwiftUI uses property wrappers to assign behaviors to properties of its views.
  • @State allows a structure to have persistent, mutable properties.
  • @Binding sends data to a view and allows that view to send any changes back.
  • You can include data files in your Swift apps and read them from the app bundle.

Where to Go From Here

Your game now works perfectly, but the sidebar is still showing the placeholder text. In the next chapter, you’ll create a new model to hold app-wide properties, including an array of games for listing in and selecting from the sidebar.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now