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

8. Showing Other Windows
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 two chapters, you set up the data flow for your app. This was a big task and one that can be confusing, so if you feel lost, don’t worry about it. Keep going and revisit these chapters at the end of this section if you need to.

Now, you’ll move on to dealing with multiple windows. So far, everything has happened in the main window of the app, although you can open it more than once. But Mac apps frequently have more than one type of window, and that’s what you’ll look into next.

First, you’ll create a Settings window to allow users to configure the app.

After that, you’ll create an entirely new window with a different SwiftUI view. And you’ll see how to pass data around between different windows.

Creating a Settings View

Launch Xcode and open the project you ended with after the last chapter. If you prefer, you can use the starter project from the downloads for this chapter, but it contains nothing new.

Press Command-R to run the app and open the Snowman menu:

Snowman menu
Snowman menu

There are the expected menu items, but no Settings… option. Go to to Xcode and open the Xcode menu. There’s a Settings… menu item under the first divider. So how can you add that to Snowman?

Open SnowmanApp.swift. The body contains a single scene that defines the main window.

Add this after the end of WindowGroup:

// 1
Settings {
  // 2
  Text("Settings View")
    .frame(width: 200, height: 100)
}

What’s this?

  1. Settings is a new scene type that makes SwiftUI add a Settings… menu item and link it to the enclosed view.
  2. For now, this is a placeholder view for the Settings window to show. It has a default frame so the window is large enough to see when it opens.

Run the app and look at the Snowman menu again:

Snowman menu with Settings.
Snowman menu with Settings.

Now it has a Settings… option and it has allocated the default keyboard shortcut: Command-,.

Select this option or press Command-, to see your new window with the placeholder text:

Settings window with placeholder.
Settings window with placeholder.

Note: Until macOS Ventura, these were Preferences windows and the system options were System Preferences. Ventura brings macOS more into line with iOS, which has always used the term Settings.

A feature of the Settings scene is that it never opens its window more than once. Press Command-, with the Settings window already open and it brings it to the front, but doesn’t duplicate it.

Configuring @AppStorage

You’ve created a Settings window and it’s linked to the correct menu item — now to add some content.

@AppStorage("minWordLength") var minWordLength = 4
@AppStorage("maxWordLength") var maxWordLength = 10.0

Adding a Stepper

Still in SettingsView.swift, replace the Text view with:

// 1
Form {
  // 2
  Stepper(
    // 3
    value: $minWordLength,
    // 4
    in: 3 ... Int(maxWordLength)
  ) {
    // 5
    Text("Minimum word length: \(minWordLength)")
  }

  // more items here
}  
SettingsView()
Stepper in Settings window.
Bnezdeq iy Pibyussq cutmas.

Adjusting the Settings Window

Earlier, I told you to turn off Stage Manager but now I want you to turn it on and configure it to test a certain behavior.

Stage Manager settings
Ztoye Qemevip solsefby

// 1
TabView
// 2
.tabItem {
  // 3
  Image(systemName: "snowflake")
  Text("Settings")
}
Tab view in Settings.
Wut kaet eg Qihcunnf.

.frame(width: 420, height: 160)
Settings preview
Jexgohvf sdiviar

Limiting the Maximum Word Length

You’ve used a Stepper to set the minimum word length. Now you’ll use a Slider to set the maximum. In a production app, you’d keep the user experience (UX) consistent and only use one type, but for a learning app like this one, it’s more interesting to see some variety.

Adding a view from the library.
Ezmeyt i toic hnup mge sepxeqc.

"Maximum word length: \(Int(maxWordLength))"
Slider
Slider autocomplete options
Xbuzak oetomajrqoji ukxueht

Slider argument errors
Mlayon angidogs iytovh

$maxWordLength
Double(minWordLength) ... 12

Using a Toggle

There’s one more setting to add: The word list contains some proper nouns — mostly place names. Your users may not want these to show up.

@AppStorage("useProperNouns") var useProperNouns = false
Toggle("Allow proper nouns", isOn: $useProperNouns)
.toggle
Toggle style options
Gajsru fjqqi abgeujp

Settings preview
Takzizrl wloviin

.formStyle(.grouped)
Settings formatted.
Yusvarwz hafyeqxob.

Applying the Settings

All these user settings change how Game selects a random word, so open Models ▸ Game.swift.

import SwiftUI
@AppStorage("minWordLength") var minWordLength = 4
@AppStorage("maxWordLength") var maxWordLength = 10
@AppStorage("useProperNouns") var useProperNouns = false
word.count >= minWordLength && word.count <= maxWordLength
// 1
.filter { word in
  // 2
  if useProperNouns {
    return true
  }
  // 3
  let firstLetter = word[word.startIndex]
  // 4
  return !firstLetter.isUppercase
}
Applying Settings
Ihqszebl Vidjozgj

Opening a Secondary Window

The Settings window is a special case, and SwiftUI provides a preset Scene for handling that. But you’’ll often want to have more than one window type in an app. You can add more scenes to the @main body to do this.

// 1
Window("Statistics", id: "stats") {
  // 2
  Text("Statistics will go here")
}
// 3
.keyboardShortcut("t", modifiers: .command)
Statistics menu item
Kketewnand mesu arum

Populating the Statistics Window

Now, you’ll replace the placeholder Text with a new view for your new window. In the Project navigator, select Views ▸ SettingsView.swift and press Command-N to make a new file.

// 1
TabView {
  // 2
  Text("Games view")
    // 3
    .tabItem {
      // 4
      Text("Games Won & Lost")
    }

  // 5
  Text("Words view")
    .tabItem {
      Text("Length of Words")
    }
}
// 6
.padding()
StatsView()
Statistics tabs with placeholders.
Cpuyorjizt vaqn debm fsexozidniyk.

Passing Data to the New Window

If this window is to show any game data, it must be able to read games from appState.

var games: [Game]
StatsView(games: [])
Xcode error fix
Pdune azmil koc

StatsView(games: appState.games)

Adding the Subviews

You’ll add two new SwiftUI view files: one for each tab. Select StatsView.swift in the Project navigator and use the technique you used before to create two new SwiftUI View files called GameStats.swift and WordStats.swift.

Statistics group
Zsifehxujq jgeat

var games: [Game]
GameStats(games: [])
WordStats(games: [])

Showing the Game Statistics

Open GameStats.swift and add this computed property:

// 1
var gameReport: String {
  // 2
  let wonGames = games.filter {
    $0.gameStatus == .won
  }
  // 3
  let lostGames = games.filter {
    $0.gameStatus == .lost
  }

  // 4
  return """
  Games won: \(wonGames.count)
  Games lost: \(lostGames.count)
  """
}
Text(gameReport)
GameStats(games: games)
Wins and Losses
Kojn ilr Xohyer

Showing the Words Statistics

Adding data to WordStats is a similar process, so open WordStats.swift now. This view will list each completed game, showing how many letters were in each word.

// 1
var wordCountReport: String {
  // 2
  let completedGames = games.filter {
    $0.gameStatus != .inProgress
  }

  // 3
  let gameReports = completedGames.map { game in
    // 4
    let statusText = game.gameStatus == .won ? "won" : "lost"
    // 5
    return "\(game.id): \(game.word.count) letters - \(statusText)"
  }

  // 6
  return gameReports.joined(separator: "\n")
}
Text(wordCountReport)
WordStats(games: games)
Word length statistics
Setp jewrzf wtuyijqijq

Key Points

  • A Settings scene adds a Settings… menu item and keyboard shortcut that you can link to any SwiftUI view to show as the user settings interface.
  • The @AppStorage property wrapper saves and restores user settings.
  • SwiftUI has a variety of input views, so you can choose the ones that suit your data types.
  • You add secondary windows using a Window scene, which adds an item to the Window menu.

Where to Go From Here

You’ve configured two different additional windows. The Settings window is complete, but the Statistics window only shows plain text reports.

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