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

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 Word Definition

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.

The game can throw up some unusual words, so it’d be nice to be able to find a definition for any that are new to you.

Dictionary.com is an online dictionary where you can look up word definitions.

You can provide the word in the web address so the user doesn’t have to type it. Test by opening this URL in your browser:

https://www.dictionary.com/browse/ethereal

You add browse to the basic address and follow it with the word to look up.

You’ll add a new window to the app to display a web view with the Dictionary.com page for the game word.

The only problem is that SwiftUI doesn’t have a web view, so you’ll have to use the one in AppKit: WKWebView.

Adding a Lookup Button

The first step is to add a button to trigger this. Like the New Game button, it’ll only be visible and active for completed games, so it makes sense to put it beside that button.

Embed in HStack
Ojbop uj HBlurm

Button("Look Up Word") {
  // open lookup window
}
HStack(spacing: 60)
Lookup button
Paanac wohpum

Creating a Web View

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 WebKit

// 2
struct WebView: NSViewRepresentable {
  // 3
  let word: String
}
Fixing the protocol error.
Bilocq zge rwuhexet invay.

typealias NSViewType = type
WKWebView
Really fixing the protocol error.
Voodbz milegn mnu zboqokoj ovsen.

Filling in the Methods

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

WKWebView()
// 1
let address = "https://www.dictionary.com/browse/\(word)"
// 2
guard let url = URL(string: address) else {
  return
}

// 3
let request = URLRequest(url: url)
// 4
nsView.load(request)

Setting Up a Window Group

Open SnowmanApp.swift and scroll to the end of the structure. You’ve already added a new Window to display the Statistics views. Now, you’ll add a WindowGroup to show the web view.

// 1
WindowGroup(for: String.self) { $word in
  // 2
  Text(word ?? "SNOWMAN")
}

Opening the Window

Open GameView.swift again and scroll to the top. Add this:

@Environment(\.openWindow) var openWindow
openWindow(value: game.word)
Lookup window test
Giumog peykop gafw

Showing the Web Page

The last part of this process is to display your WebView instead of the Text view.

WebView(word: word ?? "SNOWMAN")
Configuring the sandbox.
Vohyozifubp zca dahkteq.

Looking up a word at Dictionary.com
Heuresr ix i fixx aq Qomneoyudl.moc

.navigationTitle(word ?? "Snowman")
.defaultSize(width: 1000, height: 800)
WebView window
HexCeiy digxon

Using a Coordinator

Sometimes, you want to get data back into a SwiftUI view from an AppKit view. WKWebView can have a navigationDelegate to track navigation successes and failures. But how can you create a delegate so that both SwiftUI and AppKit can use its data?

@Binding var isLoading: Bool
class Coordinator: NSObject, WKNavigationDelegate {
}
func makeCoordinator() -> Coordinator {
  code
}
// 1
var parent: WebView

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

Setting Up the Delegate

The purpose of this coordinator is to track page loads and errors using WKNavigationDelegate.

// 1
let webView = WKWebView()
// 2
webView.navigationDelegate = context.coordinator
// 3
return webView
Adding web delegate methods.
Urdegp ruq vikirura geblepm.

parent.isLoading = false
print(error.localizedDescription)
parent.isLoading = false

Displaying a Lookup View

At the moment, your WindowGroup displays nothing but a WebView. Now, you’ll give it a new SwiftUI view that contains the WebView as well as indicators to show if the page is still loading.

let word: String
@State var webViewIsLoading = true
LookupView(word: "SNOWMAN")
// 1
ZStack {
  // 2
  WebView(word: word, isLoading: $webViewIsLoading)

  // 3
  if webViewIsLoading {
    ProgressView()
  }
}
// 4
.navigationTitle(webViewIsLoading ? "Loading…" : word)
LookupView(word: word ?? "snowman")
Loading…
Xaoluqx…

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: .keyUp) { event in
    // 2
    print(event.characters)
    // 3
    return event
  }
}
.onAppear(perform: startMonitoringKeystrokes)
Detecting key presses.
Geyicvepk nig fzepkaf.

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
}
// 1
.onChange(of: nextGuess) { _ in
  // 2
  if game.gameStatus == .inProgress {
    // 3
    game.processGuess(letter: nextGuess)
  }
  // 4
  nextGuess = ""
}
Playing with key press detection.
Cvevuwc vosw far rcakb hezosvoip.

Watching for the Command Key

Run the app and press Command-N to start a second game. Now press Command-D to get a different word. The game processed both these key presses, so it looks like you’ve already guessed N and D for the new game. And you didn’t get a different word since you already made a guess.

// 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?

Challenge

Add a menu item for looking up the game word. Don’t forget to give it a keyboard shortcut and make sure to disable it for games that are still in progress to stop your players cheating. :]

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