Chapters

Hide chapters

macOS Apprentice

Third Edition · macOS 26 · Swift 6 · Xcode 26

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

17. Using AppKit in SwiftUI
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 Section 2 of this book, you created a complete app using the SwiftUI layout framework.

SwiftUI is Apple’s newest layout system and it has some great features, but it doesn’t do everything — at least not yet.

In this chapter, you’ll learn how to integrate AppKit components into a SwiftUI app. This allows you to use SwiftUI as the basis for your app and drop into AppKit when SwiftUI is missing a feature or isn’t suited to a particular purpose.

Showing a Warning Bar

Open your Snowman project from the end of Chapter 10, “Adding Toolbars & Menus”, or use the starter project from the downloads for this chapter.

Run the app and play a few games to remind yourself what you built:

Starting app with some games played.
Starting app with some games played.

While the disappearing snowman shows your accumulating wrong guesses, it would be cool to add a warning bar with color coding to show exactly how many guesses you have left and how close you are to losing. AppKit contains an NSLevelIndicator that is perfect for this, so now you’ll learn how to incorporate one into your SwiftUI app.

Note: In previous editions of this book, you added a web view using WKWebView. SwiftUI now contains its own WebView so this is no longer a good example of using AppKit.

Creating a Level Indicator

To show any AppKit view in a SwiftUI app, you first convert it into a SwiftUI view. The NSViewRepresentable protocol provides the means for doing this.

// 1
import SwiftUI
import AppKit

// 2
struct WarningBar: NSViewRepresentable {
  // 3
  let guessesLeft: Int
}
Fixing the protocol error.
Tayuwj mku mburadeq agcep.

typealias NSViewType = type
typealias NSViewType = NSLevelIndicator
Really fixing the protocol error.
Vuugmb tadilw vwo pzobufov egfak.

Filling in the Methods

The first of these methods makes the AppKit view, so in makeNSView(context:), replace the placeholder with:

NSLevelIndicator()
nsView.intValue = Int32(guessesLeft)

Setting Up the Data Flow

Your new WarningBar structure expects an Int containing the number of guesses left, but right now, there’s no property that supplies that information directly. It sounds like something Game should do, so open Game.swift and add this computed property:

// 1
var guessesLeft: Int {
  // 2
  7 - incorrectGuessCount
}
let maxGuesses = 7
var guessesLeft: Int {
  maxGuesses - incorrectGuessCount
}

Adding the View

The logical place for this bar is underneath the snowman image inside GameView. Open GameView.swift and find the Image that displays the snowman. Select the Image line and its three modifier lines, then press {. This wraps the selected lines in a pair of curly braces, indents them and places the cursor at the front so you can type the name of a new container view.

WarningBar(guessesLeft: game.guessesLeft)
Warning bar with default settings.
Hatlecy tiw qugl kutoamn muynurcn.

Configuring the Level Indicator

NSLevelIndicator has several properties that you can change to make it look the way you want. To see the effect of your changes as you make them, open GameView.swift, resume the preview and then click the pin icon at the top left of the preview.

let levelIndicator = NSLevelIndicator()

// set properties here

return levelIndicator
levelIndicator.levelIndicatorStyle = .
let maxGuesses: Int
WarningBar(guessesLeft: game.guessesLeft, maxGuesses: game.maxGuesses)
levelIndicator.maxValue = Double(maxGuesses)
Preview after setting max value.
Bjidieg agbuh kolnahy taj lawia.

levelIndicator.warningValue = 3
levelIndicator.criticalValue = 1
Warning bar with colors.
Girwadj cur vihx jarurx.

.padding(.horizontal)
The resized warning bar.
Zvi timecaj lohcahd pur.

Using a Coordinator

You’re sending data from the SwiftUI view to the AppKit view, but what if you want to communicate back the other way?

@Binding var showHint: Bool
class Coordinator {
}
func makeCoordinator() -> Coordinator {
  code
}
// 1
var parent: WarningBar

// 2
init(_ parent: WarningBar) {
  // 3
  self.parent = parent
}
Coordinator(self)

Setting Up the Action and Target

The purpose of this coordinator is to detect and respond to clicks inside the level indicator. AppKit views do this with a target and action system. You tell the view what target should receive the click and what action this should trigger. In this case, the target will be the coordinator and the action will be a method in the coordinator.

// 1
@objc func handleClick(sender: NSLevelIndicator) {
  // 2
  parent.showHint.toggle()
}
// 1
levelIndicator.target = context.coordinator
// 2
levelIndicator.action = #selector(Coordinator.handleClick)
context.coordinator.parent = self

Showing Hints

Right now, the app doesn’t build. This is because you added a new binding property to WarningBar and that property doesn’t exist or get passed to the view yet.

var showHint = false
// 1
WarningBar(
  // 2
  guessesLeft: game.guessesLeft,
  maxGuesses: game.maxGuesses,
  // 3
  showHint: $appState.games[appState.gameIndex].showHint
)
// 1
var hint: String? {
  // 2
  if gameStatus != .inProgress || !showHint {
    return nil
  }

  // 3
  if guesses.count == 0 {
    return "Starting with a vowel is always a good idea."
  }

  // 4
  let numberOfVowelsGuessed = guesses.count {
    "AEIOU".contains($0)
  }
  if numberOfVowelsGuessed == 0 {
    return "Try guessing a vowel."
  } else if numberOfVowelsGuessed < 3 {
    return "Try guessing another vowel."
  }

  // 5
  let commonConsonants = ["T","N","S","H","R"]
  let unusedCommons = commonConsonants.filter {
    !guesses.contains($0)
  }
  if !unusedCommons.isEmpty {
    return "These are commonly used consonants that you haven't tried yet: "
      + unusedCommons
      .joined(separator: ", ")
  }

  // 6
  return "It's generally best to avoid uncommon letters like Z, Q and X."
}
// 1
var statusOrHint: String {
  // 2
  if let hint = game.hint {
    return hint
  }
  return game.statusText
}
Text(statusOrHint)
showHint = false
Asking for a hint.
Izyefq teq i yijp.

Game views folder.
Gibi leovn balcuk.

Observing Events

You’ve seen how to convert an AppKit view into a SwiftUI view for presentation in your SwiftUI app, but AppKit has more than views. One thing it’s extremely good at is event handling.

Trapping Key Strokes

Start by stripping out the views and code related to the text entry field.

func startMonitoringKeystrokes() {
  // 1
  NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
    // 2
    print(event.characters)
    // 3
    return event
  }
}
.onAppear(perform: startMonitoringKeystrokes)
Detecting key presses.
Meloswofd zuj psojpof.

Processing Key Strokes

The first step is to work out if the player pressed a valid key.

// 1
guard let key = event.characters(byApplyingModifiers: .shift) else {
  return event
}

// 2
if key >= "A" && key <= "Z" {
  // 3
  nextGuess = key
  return nil
}
// 1
.onChange(of: nextGuess) {
  // 2
  if game.gameStatus == .inProgress {
    // 3
    game.processGuess(letter: nextGuess)
  }
  // 4
  nextGuess = ""
}
Playing with key press detection.
Kkijorb caqb nej wxang yusucpiid.

Watching for the Command Key

Run the app and press Command-N to try to start a second game. Try quitting the app using Command-Q. The game intercepted these key presses, so it looks like you’ve guessed N and Q for the first game. You didn’t get a new game, and the app didn’t quit.

// 1
if event.modifierFlags.contains(.command) {
  // 2
  return event
}

When Should You Start With SwiftUI?

You’ve made a SwiftUI app and you’ve made an AppKit app. Now, you’ve learned how to include AppKit in a SwiftUI app, but when is this the right approach to take?

Challenges

  1. Add a tooltip to WarningBar that shows how it can be used to display a hint.
  2. Add a menu item to toggle the hint on and off, give it a keyboard shortcut, and make sure the menu item is disabled when the game isn’t in progress. Bonus points if you get its title to change between “Show Hint” and “Hide Hint” depending on the current state of the hint.

Key Points

  • NSViewRepresentable lets you create a SwiftUI version of an AppKit view.
  • A coordinator handles passing data back from the AppKit view to SwiftUI.
  • You can include non-view AppKit features, like NSEvent, in a SwiftUI app.
  • The hybrid approach — adding AppKit to a SwiftUI app to supply missing features — is extremely powerful, and it is frequently the best way to structure your apps.

Where to Go From Here?

In this chapter, you integrated AppKit into a SwiftUI app and looked at when this is the right approach. In the next chapter, you’ll do the reverse and bring SwiftUI into your AppKit app.

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