macOS AppKit Interoperability
The two-directional bridge — NSViewRepresentable and NSViewControllerRepresentable for embedding AppKit in SwiftUI; NSHostingController and NSHostingView for hosting SwiftUI in AppKit. Plus the responder chain, NSToolbar, NSOpenPanel, and drag-and-drop bridging.
When to Use This Skill
Use this skill when you're:
- Embedding an AppKit view or view controller inside SwiftUI
- Hosting SwiftUI views inside an AppKit app
- Working around SwiftUI gaps —
NSToolbarcustomization,NSOpenPaneloptions,NSTextViewrich text - Debugging menu commands, copy/paste, or keyboard shortcuts that don't cross the SwiftUI/AppKit boundary
- Bridging drag-and-drop between SwiftUI's
Transferablemodel and AppKit'sNSDraggingDestination - Diagnosing responder chain or focus behavior that breaks when mixing frameworks
- Optimizing SwiftUI cells inside
NSCollectionVieworNSTableViewfor scroll performance
Example Prompts
Questions you can ask Claude that will draw from this skill:
- "How do I host an
NSTextViewinside SwiftUI and keep bindings synced?" - "My SwiftUI cells inside
NSCollectionViewcause scroll jank. What's wrong?" - "When should I drop from
.fileImportertoNSOpenPanel?" - "My
.onCommandmodifier is silently ignored. What did I miss?" - "Why are my writes from the AppKit delegate not reaching the SwiftUI binding?"
What This Skill Provides
Direction Decision
- SwiftUI host + AppKit guest →
NSViewRepresentable(raw view) orNSViewControllerRepresentable(controller with lifecycle) - AppKit host + SwiftUI guest →
NSHostingController(controller contexts) orNSHostingView(raw view contexts) - When to bridge at all — start with SwiftUI; cross only when SwiftUI lacks the capability
NSViewRepresentable Lifecycle
makeCoordinator()→makeNSView(context:)→updateNSView(_:context:)→dismantleNSView(_:coordinator:)- The coordinator pattern for delegate callbacks writing back to bindings
- Refreshing
context.coordinator.parent = selfinupdateNSViewso bindings stay current - Guarding redundant property sets to avoid unnecessary AppKit work
- Reading
context.environment(e.g.,isEnabled) and applying it to the AppKit view - The never-set-frame rule — SwiftUI owns layout; use
.frame()on the SwiftUI side
NSHostingController vs NSHostingView
NSHostingControllerfor view-controller contexts (NSSplitViewItem, sheets, popovers, modal windows, tabs)NSHostingViewfor raw view contexts (collection cells, sidebars, table cells)sizingOptionson the controller for Auto Layout constraint generation- Critical reuse rule: create the hosting view once, then update
rootViewon reuse — never new-hosting-view-per-cell
Responder Chain and Focus
- The "they don't live in separate worlds" mental model — SwiftUI views participate in the AppKit responder chain
- SwiftUI command modifiers —
.copyable,.cuttable,.pasteDestination,.onMoveCommand,.onExitCommand,.onCommand(#selector(...)) - The
.focusable()requirement for command receivers - Full Keyboard Navigation testing (System Settings toggle on and off)
Bridging Other AppKit APIs
- NSToolbar for capabilities
.toolbardoesn't cover (item validation, user customization, centered groups) NSOpenPanelfor capabilities.fileImporterdoesn't cover (directories, accessory views, ubiquitous content)- Drag and drop —
Transferable+.draggable/.dropDestinationfor SwiftUI-native;NSDraggingDestinationon the AppKit view inside a representable - Shared state via
@Observable(orObservableObject) accessible to both sides
Key Pattern
The most common performance bug — creating a new NSHostingView on every cell reuse instead of updating rootView:
class ShortcutItemView: NSCollectionViewItem {
private var hostingView: NSHostingView<ShortcutView>?
func displayShortcut(_ shortcut: Shortcut) {
let view = ShortcutView(shortcut: shortcut)
if let hostingView {
hostingView.rootView = view // reuse — SwiftUI diffs internally
} else {
let newHosting = NSHostingView(rootView: view)
self.view.addSubview(newHosting)
setupConstraints(for: newHosting)
hostingView = newHosting
}
}
}In updateNSView, always refresh context.coordinator.parent = self so coordinator-held bindings stay current — stale references silently swallow writes back to SwiftUI state.
Documentation Scope
This page documents the appkit-interop skill in the axiom-macos suite. The skill file contains comprehensive guidance Claude uses when answering your questions about bridging SwiftUI and AppKit.
For UIKit-SwiftUI bridging — Use axiom-uikit for the same Representable pattern with UIView/UIViewController types.
Related
- swiftui-differences — Drop to AppKit only when these macOS SwiftUI primitives don't cover the need
- windows —
NSHostingControlleris the right way to host SwiftUI inside an AppKit-managed window or sheet - sandbox-and-file-access — Reasons to drop from
.fileImportertoNSOpenPanel - menus-and-commands — Where SwiftUI's command modifiers meet AppKit's responder chain
- axiom-uikit — Same representable pattern, UIKit edition
Resources
WWDC: 2022-10075
Docs: /swiftui/nsviewrepresentable, /swiftui/nsviewcontrollerrepresentable, /swiftui/nshostingcontroller, /swiftui/nshostingview, /appkit/nstoolbar, /appkit/nsopenpanel
Skills: axiom-macos, swiftui-differences, windows, sandbox-and-file-access, menus-and-commands