How to present a SwiftUI scene from AppKit

Issue #1053

Plenty of Mac apps still run on an NSApplicationDelegate. The lifecycle works, the windows are wired up, and rewriting everything into the SwiftUI App protocol just to gain one settings window is hard to justify. The friction shows up the moment you want a modern scene like Settings or MenuBarExtra, because those are SwiftUI scenes and a SwiftUI scene normally lives inside a SwiftUI App.

NSHostingSceneRepresentation removes that wall. It wraps one or more SwiftUI scenes and lets you attach them to an AppKit app at launch, so your existing delegate stays in charge while SwiftUI manages the new scenes. You adopt SwiftUI where it pays off and leave the rest of the app untouched.

Attaching a scene at launch

The work happens in applicationWillFinishLaunching, before the app finishes coming up. You build a representation from a scene builder closure, then hand it to NSApplication.

import AppKit
import SwiftUI

@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
    let store = NoteStore()

    func applicationWillFinishLaunching(_ notification: Notification) {
        let scenes = NSHostingSceneRepresentation {
            CaptureMenuBar(store: store)
            CaptureSettings(store: store)
        }
        NSApplication.shared.addSceneRepresentation(scenes)
    }
}

The closure is a scene builder, so you can list several scenes and they all come along. Each one is an ordinary SwiftUI Scene, written exactly as it would be inside a SwiftUI App.

struct CaptureMenuBar: Scene {
    let store: NoteStore

    var body: some Scene {
        MenuBarExtra("Quick Note", systemImage: "square.and.pencil") {
            QuickNoteView(store: store)
        }
        .menuBarExtraStyle(.window)
    }
}

struct CaptureSettings: Scene {
    let store: NoteStore

    var body: some Scene {
        Settings {
            SettingsView(store: store)
        }
    }
}

Notice the shared store. Mark your model @Observable and the same instance can drive both the SwiftUI scenes and any AppKit views you already have, so state stays consistent across both worlds.

Opening settings from AppKit code

A settings window is only useful if something can open it. In a pure SwiftUI app you reach for the openSettings action from the environment, but here the trigger lives in AppKit, often the standard menu item connected to openSettings(_:). The representation exposes its environment, which gives you the same action.

@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
    let store = NoteStore()
    private var openSettings: (() -> Void)?

    func applicationWillFinishLaunching(_ notification: Notification) {
        let scenes = NSHostingSceneRepresentation {
            CaptureMenuBar(store: store)
            CaptureSettings(store: store)
        }
        NSApplication.shared.addSceneRepresentation(scenes)

        openSettings = { scenes.environment.openSettings() }
    }

    @IBAction func showSettings(_ sender: Any?) {
        openSettings?()
    }
}

Capturing the closure once keeps the call site clean and bridges the AppKit action straight into the SwiftUI scene. The menu item fires, the closure runs, and SwiftUI brings up the settings window.

Inserting and removing a scene at runtime

Some scenes should not always exist. A menu bar item is a good example, since many users want to hide it. MenuBarExtra takes an isInserted binding, and when you bind it to a property on your observable model the scene appears and disappears on its own.

struct CaptureMenuBar: Scene {
    @Bindable var store: NoteStore

    var body: some Scene {
        MenuBarExtra(
            "Quick Note",
            systemImage: "square.and.pencil",
            isInserted: $store.showsMenuBar
        ) {
            QuickNoteView(store: store)
        }
        .menuBarExtraStyle(.window)
    }
}

A toggle in your settings flips showsMenuBar, and the menu bar item follows without any manual window management.

struct SettingsView: View {
    @Bindable var store: NoteStore

    var body: some View {
        Form {
            Toggle("Show in menu bar", isOn: $store.showsMenuBar)
        }
        .formStyle(.grouped)
    }
}

Read more

For the full API surface, see Apple’s documentation for NSHostingSceneRepresentation and the session Use SwiftUI with AppKit and UIKit.

Written by

I’m open source contributor, writer, speaker and product maker.

Start the conversation