13 December 2020

Creating a SwiftUI Window in an Objective-C AppKit App

I’ve been “rehabbing” a legacy Objective-C app (the Unbound photo browser), trying to make up for a couple years of neglect, and I wanted to start building new views in SwiftUI. There are a lot of good tutorials online for hosting SwiftUI views in UIKit apps (either iOS/iPadOs or macOS + Catalyst), but not much on how to do this for Mac and AppKit… and especially not when you’re still mostly Obj-C!

The first thing to know is: you can’t create SwiftUI Views directly from Objective-C—you’ll have to call into a Swift wrapper. (Adding Swift into an existing Obj-C project is beyond the scope of this post, but there are tutorials available elsewhere.)

In my case, I was trying to create a little preferences window in SwiftUI, so needed to create an NSWindowController to hold the View. This meant I needed:

  1. A plain old NSWindowController
  2. …containing an NSHostingController (i.e., an AppKit view controller used to host a SwiftUI view hierarchy—this is the AppKit equivalent of UIKit’s UIHostingController)
  3. …whose root was my SwiftUI View

Here’s what that looks like:

import Cocoa
import SwiftUI

class SwiftUIWindowCtrl<RootView: View>: NSWindowController {
    convenience init(rootView: RootView) {
        let hostingCtrl = NSHostingController(rootView: rootView.frame(width: 400, height: 300))
        let window = NSWindow(contentViewController: hostingCtrl)
        window.setContentSize(NSSize(width: 400, height: 300))
        self.init(window: window)
    }
}

@objc class PrefsWindowObjCBridge: NSView {
    @objc class func makePrefsWindow() -> NSWindowController {
        SwiftUIWindowCtrl(rootView: PrefWindowView())
    }
}

From there, I could call it from my Objective-C AppDelegate like this:

#import "MyApp-Swift.h" // the autogenerated Swift bridging header
. . .
self.prefsWindow = [PrefsWindowObjCBridge makePrefsWindow];
[self.prefsWindow showWindow:self];

Of course, if you’re just dropping your View into an existing hierarchy, you can use just an NSHostingController and skip the NSWindowController.

Happy hacking!