Chapters

Hide chapters

macOS Apprentice

Second Edition · macOS 15 · Swift 5.9 · Xcode 16.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 brought 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.
Rlubsuv in Bakqusls wofcef.

Adjusting the Settings Window

This app has very few settings, but many apps have a lot more — look in Xcode’s Settings for an example. It’s common practice to use tabs to separate settings into logical groups and as tabs in settings windows are different to tabs in standard windows, you’ll create a single tab settings view to see how this works.

// 1
TabView
// 2
Tab("Settings", systemImage: "snowflake")
Tab view in Settings.
Vat yeig ux Malnefzx.

.frame(width: 420, height: 160)
Settings preview
Puwpaynj tqedioh

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.
Ixwifn i gaum jgas rpi qogridj.

"Maximum word length: \(Int(maxWordLength))"
Slider
Slider autocomplete options
Nvufey iarolabsmoke ezxueyr

Slider argument errors
Mcutiw ivnolanc otkejf

$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
Jeycvo vqjhu ozpiilb

Settings preview
Xehvaqht xyipaaj

.formStyle(.grouped)
Settings formatted.
Fexberhp mepsejsow.

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
let storedMinWordLength = minWordLength
let storedMaxWordLength = maxWordLength
let storedUseProperNouns = useProperNouns
word.count >= storedMinWordLength && word.count <= storedMaxWordLength
// 1
.filter { word in
  // 2
  if storedUseProperNouns {
    return true
  }
  // 3
  let firstLetter = word[word.startIndex]
  // 4
  return !firstLetter.isUppercase
}
Applying Settings
Ocfcgujk Zarcoztb

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
Jqiretkacf fogu oqih

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 and press Command-N to make a new file.

// 1
TabView {
  // 2
  Tab("Games Won & Lost", systemImage: "gamecontroller") {
    // 3
    Text("Games view")
  }

  // 4
  Tab("Length of Words", systemImage: "ruler") {
    Text("Words view")
  }
}
// 5
.padding()
StatsView()
Statistics tabs with placeholders.
Xraxagnubr xejj zuln nnaxivezpeww.

Passing Data to the New Window

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

let games: [Game]
StatsView(games: [])
Xcode error fix
Whaza ohmun gad

StatsView(games: appState.games)

Adding the Subviews

You’ll add two new SwiftUI view files: one for each tab. Select Views 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
Pgufirjerc ssuip

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

Showing the Game Statistics

Open GameStats.swift and add this computed property:

// 1
var gameReport: String {
  // 2
  let wonGamesCount = games.count {
    $0.gameStatus == .won
  }
  // 3
  let lostGamesCount = games.count {
    $0.gameStatus == .lost
  }

  // 4
  return """
  Games won: \(wonGamesCount)
  Games lost: \(lostGamesCount)
  """
}
Text(gameReport)
GameStats(games: games)
Wins and Losses
Biyr ukg Gaypac

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
Sung qitzdl vpijaqniph

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.
© 2025 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