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

18. Using SwiftUI in AppKit
Written by Sarah Reichelt

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapter, you went back to your SwiftUI app and used AppKit to add some features that aren’t available in SwiftUI.

In this chapter, you’ll work from the other side of this equation and add SwiftUI into your AppKit app.

When coding a SwiftUI app, there are things you can’t do without AppKit. In an app that starts with AppKit, there are almost no features you can’t add — the new SwiftUI Charts library is the only thing I can think of — but there are cases where SwiftUI can make your life easier or make developing a certain feature quicker.

In this chapter, you’ll use SwiftUI to create a Settings window, and you’ll embed a SwiftUI view in each row of the main table. Then, you’ll examine when this is the best way to build an app.

Adding a Hosting Controller

In the previous chapter, you used NSViewRepresentable to create a SwiftUI view from an AppKit view. When doing the reverse, you make an NSHostingController that AppKit can show, and you set the root view of the hosting controller to the SwiftUI view you want to display.

Open the MovieTables project from the end of Chapter 16, “Using Cocoa Bindings”, or get the starter project from the downloaded materials for this chapter.

Run the app and take a look at the MovieTables menu:

MovieTables menu
MovieTables menu

There’s a Settings… menu item, but you haven’t linked it to a window, so your app disables the item.

Open Main.storyboard and, in Application Scene, open the MovieTables menu. You’re probably surprised to see a Preferences… menu item there. Until macOS Ventura, Settings were called Preferences. Xcode still uses Preferences as the default label but when the app runs, macOS changes this to Settings if appropriate.

Open the Library using the + button in the toolbar or by pressing Shift-Command-L. Search for host and drag a Hosting View Controller into the storyboard, near the menu bar:

Adding a Hosting View Controller.
Adding a Hosting View Controller.

With that in place, Control-drag from the Preferences… menu item to the new Hosting Controller and select Show from the popup options:

Creating a segue.
Creating a segue.

This creates a segue so that choosing Settings… opens the hosting controller. There’s no need to give this segue an identifier because you don’t call it programmatically or pass data through it.

Next, you’ll make a SwiftUI view for the hosting controller to host.

Creating the Settings View

The Settings view will be pure SwiftUI. Select EditViewController.swift in the Project navigator and press Command-N to add a file. Select SwiftUI View and set the name to SettingsView.swift.

@AppStorage("defaultViewMode") var defaultViewMode = ViewMode.allMovies
@AppStorage("highRatingLimit") var highRatingLimit = 9.0
enum ViewMode: Int {

Coding the Interface

Replace the default contents of body with:

// 1
VStack(alignment: .leading, spacing: 30) {
  // 2
  Picker("Default View Mode:", selection: $defaultViewMode) {
    // 3
    Text("All Movies").tag(ViewMode.allMovies)
    Text("Favorites Only").tag(ViewMode.favsOnly)
    Text("Highest Rated").tag(ViewMode.highRating)
  }
  // 4
  .pickerStyle(.radioGroup)

  // 5
  Slider(value: $highRatingLimit, in: 7.5 ... 10.0) {
    // 6
    HStack {
      Text("High Rating Limit:")
      // 7
      Text(highRatingLimit, format: .number.precision(.fractionLength(1)))
    }
  }
}
// 8
.frame(width: 300, height: 120)
.padding()
Previewing the Settings view.
Vnujeigadq tso Jibmekln jeud.

Customizing the Hosting Controller

An NSHostingController has a rootView property to tell it what SwiftUI view to host. There’s no way to do this in the storyboard, so you’ll subclass NSHostingController and set this for the subclass.

// 1
import SwiftUI

// 2
class SettingsHostingController: NSHostingController {
  // 3
  required init?(coder: NSCoder) {
    // 4
    super.init(coder: coder, rootView: SettingsView())
  }
}
Setting host controller class.
Decpavj sorl wewdpubdam tkuqh.

Setting hosting controller attributes.
Lalqinj lezwemf yodsnulgow uscbesigup.

class SettingsHostingController: NSHostingController<SettingsView> {
Opening the Settings view.
Ocihaqp wgu Lubrucns beib.

Using a Setting in AppKit

The Settings window stores two different settings using the @AppStorage property wrapper. This is a SwiftUI wrapper built on top of AppKit’s UserDefaults system, so you can access them this way.

// 1
super.viewDidLoad()
movies = dataStore.readStoredData()

// 2
let defaultViewModeSetting = UserDefaults.standard
  .integer(forKey: "defaultViewMode")
// 3
if let defaultViewMode = ViewMode(rawValue: defaultViewModeSetting) {
  // 4
  viewMode = defaultViewMode
}

// 5
addSortDescriptors()
searchMovies()
showMovieCount()
Default view mode.
Nilough veew hazo.

Watching for a Change

Whenever the user changes the high rating limit, they expect the new setting to take effect immediately. You can’t ask them to quit the app and restart to implement their change.

// 1
NotificationCenter.default.addObserver(
  // 2
  forName: UserDefaults.didChangeNotification,
  // 3
  object: nil,
  // 4
  queue: .main) { _ in
    // 5
    // process notification
}

Processing the New Setting

Add a new ViewController method:

func userDefaultsChanged() {
  // 1
  let newLimit = UserDefaults.standard.double(forKey: "highRatingLimit")
  // 2
  let roundedLimit = round(newLimit * 10) / 10
  // 3
  highRatingLimit = roundedLimit

  // 4
  if viewMode == .highRating {
    searchMovies()
  }
}
self.userDefaultsChanged()
UserDefaults.standard.register(defaults: ["highRatingLimit": 9.0])
Changing the high rating limit setting.
Tpofdexy wxi nutl digath hahaj muhzusd.

Jazzing Up the Table

Run the app if it isn’t still running and take a look at the main movies table:

The movies table
Hdu bojoek ponvu

Turning on alternating rows.
Bulcivf iv eqmarsihatw bind.

Setting the row size
Sejbedg pse ziv hami

Table with alternating and taller rows.
Xidhi hetc uqlaccexejy afh tefhev rilj.

Designing a Rating Cell

Frequently, an app or web page shows a rating as a line of stars with some colored or filled in to indicate the rating. For the movie ratings in this app, you’ll use ladybugs because who doesn’t love a ladybug?

Searching SF Symbols.
Laoctpugg ZG Clzxixx.

Creating the Rating View

Select SettingsView.swift in the Project navigator and then create a new SwiftUI View file called RatingView.swift. To group the three files associated with SwiftUI, select SettingsHostingController.swift, SettingsView.swift and RatingView.swift in the Project navigator. Right-click and choose New Group from Selection setting the name of the new group to SwiftUI. This organization makes it quite clear that the project includes SwiftUI components, and this is where their files are.

// 1
let rating: Double
// 2
let ladybugImage = Image(systemName: "ladybug")
  .symbolRenderingMode(.multicolor)
// 1
HStack(spacing: 2) {
  // 2
  ladybugImage.symbolVariant(rating >= 8 ? .fill : .none)
  ladybugImage.symbolVariant(rating >= 8.5 ? .fill : .none)
  ladybugImage.symbolVariant(rating >= 9 ? .fill : .none)
  ladybugImage.symbolVariant(rating >= 9.5 ? .fill : .none)
  ladybugImage.symbolVariant(rating >= 10 ? .fill : .none)
}
// 3
.font(.title3)
// 4
.foregroundColor(.secondary)
VStack {
  RatingView(rating: 7.5)
  RatingView(rating: 8)
  RatingView(rating: 8.5)
  RatingView(rating: 9)
  RatingView(rating: 9.5)
  RatingView(rating: 10)
}
Previewing the RatingView.
Fmibiogidy tlu WahopgVuov.

Inserting SwiftUI into the Table

You used NSHostingViewController to display a SwiftUI view in its own window, as if it was a view controller. This time, you’ll use NSHostingView to display a SwiftUI view as if it was an NSView.

import SwiftUI
// 1
let view = RatingView(rating: movie.rating)
// 2
let host = NSHostingView(rootView: view)
// 3
return host
SwiftUI rating view
QpexrAE xucaty naan

When Should You Start With AppKit?

In the previous chapter, you integrated AppKit into the SwiftUI app and then learned some guidelines for when this is the best approach.

Minimum System Versions

There’s one other consideration when picking what sort of app to build, and that’s to decide the oldest version of macOS you want your app to support. Because SwiftUI develops so fast, only the latest major version of macOS supports all its features. You can build SwiftUI apps for macOS 10.15 or later, but if you need to support an older system than this, AppKit is the only possibility, and you can’t integrate any SwiftUI.

Build errors for macOS 11.0
Juitl utsojy vic farAK 40.2

In Summary

  • To support old versions of macOS, use AppKit.
  • For long-form text editing or for thousands of records, use AppKit.
  • For existing AppKit apps, add SwiftUI gradually.
  • For everything else, start with SwiftUI and include AppKit as needed.

Key Points

  • Use NSHostingController to embed a SwiftUI view in its own view controller for display in an AppKit window.
  • Both @AppStorage and UserDefaults handle user settings.
  • NSHostingView lets you insert a SwiftUI view in place of an NSView.
  • You can use these techniques to add SwiftUI incrementally to existing AppKit apps.

Where to Go From Here

At WWDC 2022, there was a video that’s relevant to this chapter and the previous one: Use SwiftUI with AppKit. Despite the title, it also covers using AppKit with SwiftUI.

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now