All skills
rshankras avatar

/appkit-swiftui-bridge

@1361023

Expert guidance for hybrid AppKit-SwiftUI development. Covers NSViewRepresentable, hosting controllers, and state management between frameworks. Use when bridging AppKit and SwiftUI.

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/appkit-swiftui-bridge

This session only. Nothing lands on disk.

hosting-controllers.md

≈1.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Hosting Controllers

Embedding SwiftUI views inside AppKit applications using NSHostingView and NSHostingController. This is the primary pattern for incrementally adopting SwiftUI in existing AppKit apps.

NSHostingView

Wraps a SwiftUI view as an NSView. Use when you need to embed SwiftUI in an existing NSView hierarchy.

Basic Usage

import SwiftUI

let swiftUIView = MySwiftUIView(viewModel: viewModel)
let hostingView = NSHostingView(rootView: swiftUIView)

// Add to existing view hierarchy
parentView.addSubview(hostingView)

// With Auto Layout
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    hostingView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
    hostingView.trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
    hostingView.topAnchor.constraint(equalTo: parentView.topAnchor),
    hostingView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor)
])

Sizing Behavior

NSHostingView calculates its intrinsicContentSize from the SwiftUI view. Control this with sizingOptions:

let hostingView = NSHostingView(rootView: myView)

// Default: hosting view has intrinsic size from SwiftUI content
hostingView.sizingOptions = .intrinsicContentSize

// Prefer minimal size (useful for fixed-size badges, indicators)
hostingView.sizingOptions = .minSize

// Both (most flexible)
hostingView.sizingOptions = [.intrinsicContentSize, .minSize]

Updating the Root View

When your data model changes, update the hosted view:

// With @Observable (macOS 14+) - automatic updates, no manual refresh needed
@Observable class ViewModel {
    var title = "Hello"
}

let viewModel = ViewModel()
let hostingView = NSHostingView(rootView: ContentView(viewModel: viewModel))
// Changes to viewModel.title automatically update the hosted SwiftUI view

// Without @Observable - manually update rootView
hostingView.rootView = MySwiftUIView(updatedData: newData)

NSHostingController

Wraps a SwiftUI view as an NSViewController. Use when you need a full view controller (e.g., in NSSplitViewController, tab views, sheets).

Basic Usage

let hostingController = NSHostingController(rootView: SettingsView())

// Present as sheet
parentViewController.presentAsSheet(hostingController)

// Add as child view controller
parentVC.addChild(hostingController)
parentVC.view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    hostingController.view.leadingAnchor.constraint(equalTo: parentVC.view.leadingAnchor),
    hostingController.view.trailingAnchor.constraint(equalTo: parentVC.view.trailingAnchor),
    hostingController.view.topAnchor.constraint(equalTo: parentVC.view.topAnchor),
    hostingController.view.bottomAnchor.constraint(equalTo: parentVC.view.bottomAnchor)
])

Window Management

Create a new window with SwiftUI content:

func showSwiftUIWindow() {
    let hostingController = NSHostingController(rootView: DetailView())

    let window = NSWindow(contentViewController: hostingController)
    window.title = "Detail"
    window.setContentSize(NSSize(width: 600, height: 400))
    window.styleMask = [.titled, .closable, .resizable, .miniaturizable]
    window.center()
    window.makeKeyAndOrderFront(nil)

    // Retain the window controller
    let windowController = NSWindowController(window: window)
    windowController.showWindow(nil)
}

In NSSplitViewController

class MainSplitViewController: NSSplitViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Sidebar in SwiftUI
        let sidebarItem = NSSplitViewItem(
            sidebarWithViewController: NSHostingController(rootView: SidebarView())
        )
        sidebarItem.minimumThickness = 200
        sidebarItem.canCollapse = true

        // Content in SwiftUI
        let contentItem = NSSplitViewItem(
            viewController: NSHostingController(rootView: ContentView())
        )
        contentItem.minimumThickness = 300

        addSplitViewItem(sidebarItem)
        addSplitViewItem(contentItem)
    }
}

Incremental Adoption Strategy

Phase 1: Leaf Views

Start by replacing simple, self-contained views:

  • Settings panels
  • Detail views
  • Empty states
  • Status indicators
// Replace an AppKit detail view with SwiftUI
class DetailViewController: NSViewController {
    private var hostingView: NSHostingView<DetailSwiftUIView>!

    override func loadView() {
        let swiftUIView = DetailSwiftUIView(item: item)
        hostingView = NSHostingView(rootView: swiftUIView)
        self.view = hostingView
    }
}

Phase 2: Container Views

Move to views that contain other views:

  • Tab containers
  • Split view panels
  • List/detail patterns

Phase 3: Window-Level

Eventually host entire windows in SwiftUI:

  • New windows as SwiftUI WindowGroup
  • Settings via SwiftUI Settings scene
  • Menu bar with MenuBarExtra

Phase 4: Full Migration

  • Replace the App Delegate entry point with SwiftUI @main App
  • Use NSApplicationDelegateAdaptor for remaining AppKit lifecycle needs
@main
struct MyApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        Settings {
            SettingsView()
        }
    }
}

SwiftUI Environment in Hosted Views

Hosted SwiftUI views have access to the full SwiftUI environment:

let hostingController = NSHostingController(
    rootView: MyView()
        .environment(\.managedObjectContext, persistentContainer.viewContext)
        .environment(appState)
)

Toolbar Integration

NSHostingController integrates with NSWindow toolbars. Use SwiftUI's .toolbar modifier:

struct ContentView: View {
    var body: some View {
        MainContent()
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button("Add", systemImage: "plus") { }
                }
            }
    }
}
// When hosted in NSHostingController, toolbar items appear in the window toolbar

Best Practices

  1. Use @Observable for shared state - Automatic updates across the bridge (macOS 14+)
  2. Set sizing options explicitly - Don't rely on default sizing for complex layouts
  3. Adopt incrementally - Start with leaf views, work up to containers
  4. Keep the bridge thin - Don't build complex logic at the boundary
  5. Use NSHostingController for view controller contexts - Sheets, split views, tab views
  6. Use NSHostingView for view-level embedding - Cells, decorations, inline content
  7. Test resizing behavior - Verify hosted views respond correctly to window resizing

Source: SKILL.md on GitHub

No alerts2mo3 checks · Risk SAFE
  • Gen Agent Trust Hub2mo

    This skill provides expert guidance for Apple developers working with hybrid AppKit and SwiftUI applications. It contains educational content and code snippets for standard framework bridging protocols without any security risks or malicious behavior.

  • Socket2mo

    No alerts

  • Snyk2mo

    Risk: LOW · No issues

Signed by skilld at 1361023. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 3 months ago
What it can do
Reads files
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 3 allowed tools
ReadGlobGrep

README badge

README badge for rshankras/claude-code-apple-skills/appkit-swiftui-bridge