# AppKit Integration When and how to use AppKit alongside SwiftUI for advanced functionality. Use AppKit (not SwiftUI) when you need: - Custom drawing with `NSView.draw(_:)` - Complex text editing (`NSTextView`) - Drag and drop with custom behaviors - Low-level event handling - Popovers with specific positioning - Custom window chrome - Backward compatibility (< macOS 13) **Anti-pattern: Using AppKit to "fix" SwiftUI** Before reaching for AppKit as a workaround: 1. Search your SwiftUI code for what's declaratively controlling the behavior 2. SwiftUI wrappers (NSHostingView, NSViewRepresentable) manage their wrapped AppKit objects 3. Your AppKit code may run but be overridden by SwiftUI's declarative layer 4. Example: Setting `NSWindow.minSize` is ignored if content view has `.frame(minWidth:)` **Debugging mindset:** - SwiftUI's declarative layer = policy - AppKit's imperative APIs = implementation details - Policy wins. Check policy first. Prefer SwiftUI for everything else. ```swift import SwiftUI struct CustomCanvasView: NSViewRepresentable { @Binding var drawing: Drawing func makeNSView(context: Context) -> CanvasNSView { let view = CanvasNSView() view.delegate = context.coordinator return view } func updateNSView(_ nsView: CanvasNSView, context: Context) { nsView.drawing = drawing } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, CanvasDelegate { var parent: CustomCanvasView init(_ parent: CustomCanvasView) { self.parent = parent } func canvasDidUpdate(_ drawing: Drawing) { parent.drawing = drawing } } } ``` ```swift struct IntrinsicSizeView: NSViewRepresentable { let text: String func makeNSView(context: Context) -> NSTextField { let field = NSTextField(labelWithString: text) field.setContentHuggingPriority(.required, for: .horizontal) return field } func updateNSView(_ nsView: NSTextField, context: Context) { nsView.stringValue = text } func sizeThatFits(_ proposal: ProposedViewSize, nsView: NSTextField, context: Context) -> CGSize? { nsView.fittingSize } } ``` ```swift import AppKit class CanvasNSView: NSView { var drawing: Drawing = Drawing() { didSet { needsDisplay = true } } weak var delegate: CanvasDelegate? override var isFlipped: Bool { true } // Use top-left origin override func draw(_ dirtyRect: NSRect) { guard let context = NSGraphicsContext.current?.cgContext else { return } // Background NSColor.windowBackgroundColor.setFill() context.fill(bounds) // Draw content for path in drawing.paths { context.setStrokeColor(path.color.cgColor) context.setLineWidth(path.lineWidth) context.addPath(path.cgPath) context.strokePath() } } // Mouse handling override func mouseDown(with event: NSEvent) { let point = convert(event.locationInWindow, from: nil) drawing.startPath(at: point) needsDisplay = true } override func mouseDragged(with event: NSEvent) { let point = convert(event.locationInWindow, from: nil) drawing.addPoint(point) needsDisplay = true } override func mouseUp(with event: NSEvent) { drawing.endPath() delegate?.canvasDidUpdate(drawing) } override var acceptsFirstResponder: Bool { true } } protocol CanvasDelegate: AnyObject { func canvasDidUpdate(_ drawing: Drawing) } ``` ```swift class KeyHandlingView: NSView { var onKeyPress: ((NSEvent) -> Bool)? override var acceptsFirstResponder: Bool { true } override func keyDown(with event: NSEvent) { if let handler = onKeyPress, handler(event) { return // Event handled } super.keyDown(with: event) } override func flagsChanged(with event: NSEvent) { // Handle modifier key changes if event.modifierFlags.contains(.shift) { // Shift pressed } } } ``` ```swift struct RichTextEditor: NSViewRepresentable { @Binding var attributedText: NSAttributedString var isEditable: Bool = true func makeNSView(context: Context) -> NSScrollView { let scrollView = NSTextView.scrollableTextView() let textView = scrollView.documentView as! NSTextView textView.delegate = context.coordinator textView.isEditable = isEditable textView.isRichText = true textView.allowsUndo = true textView.usesFontPanel = true textView.usesRuler = true textView.isRulerVisible = true // Typography textView.textContainerInset = NSSize(width: 20, height: 20) textView.font = .systemFont(ofSize: 14) return scrollView } func updateNSView(_ nsView: NSScrollView, context: Context) { let textView = nsView.documentView as! NSTextView if textView.attributedString() != attributedText { textView.textStorage?.setAttributedString(attributedText) } } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, NSTextViewDelegate { var parent: RichTextEditor init(_ parent: RichTextEditor) { self.parent = parent } func textDidChange(_ notification: Notification) { guard let textView = notification.object as? NSTextView else { return } parent.attributedText = textView.attributedString() } } } ``` Use SwiftUI views in AppKit: ```swift import AppKit import SwiftUI class MyWindowController: NSWindowController { convenience init() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), styleMask: [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: false ) // SwiftUI content in AppKit window let hostingView = NSHostingView( rootView: ContentView() .environment(appState) ) window.contentView = hostingView self.init(window: window) } } // In toolbar item class ToolbarItemController: NSToolbarItem { override init(itemIdentifier: NSToolbarItem.Identifier) { super.init(itemIdentifier: itemIdentifier) let hostingView = NSHostingView(rootView: ToolbarButton()) view = hostingView } } ``` ```swift class DraggableView: NSView, NSDraggingSource { var item: Item? override func mouseDown(with event: NSEvent) { guard let item = item else { return } let pasteboardItem = NSPasteboardItem() pasteboardItem.setString(item.id.uuidString, forType: .string) let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem) draggingItem.setDraggingFrame(bounds, contents: snapshot()) beginDraggingSession(with: [draggingItem], event: event, source: self) } func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation { context == .withinApplication ? .move : .copy } func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) { if operation == .move { // Remove from source } } private func snapshot() -> NSImage { let image = NSImage(size: bounds.size) image.lockFocus() draw(bounds) image.unlockFocus() return image } } ``` ```swift class DropTargetView: NSView { var onDrop: (([String]) -> Bool)? override func awakeFromNib() { super.awakeFromNib() registerForDraggedTypes([.string, .fileURL]) } override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { .copy } override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { let pasteboard = sender.draggingPasteboard if let urls = pasteboard.readObjects(forClasses: [NSURL.self]) as? [URL] { return onDrop?(urls.map { $0.path }) ?? false } if let strings = pasteboard.readObjects(forClasses: [NSString.self]) as? [String] { return onDrop?(strings) ?? false } return false } } ``` ```swift class CustomWindow: NSWindow { override init( contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool ) { super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) // Transparent titlebar titlebarAppearsTransparent = true titleVisibility = .hidden // Full-size content styleMask.insert(.fullSizeContentView) // Custom background backgroundColor = .windowBackgroundColor isOpaque = false } } ``` ```swift struct WindowAccessor: NSViewRepresentable { var callback: (NSWindow?) -> Void func makeNSView(context: Context) -> NSView { let view = NSView() DispatchQueue.main.async { callback(view.window) } return view } func updateNSView(_ nsView: NSView, context: Context) {} } // Usage struct ContentView: View { var body: some View { MainContent() .background(WindowAccessor { window in window?.titlebarAppearsTransparent = true }) } } ``` ```swift class PopoverController { private var popover: NSPopover? func show(from view: NSView, content: some View) { let popover = NSPopover() popover.contentViewController = NSHostingController(rootView: content) popover.behavior = .transient popover.show( relativeTo: view.bounds, of: view, preferredEdge: .minY ) self.popover = popover } func close() { popover?.close() popover = nil } } // SwiftUI wrapper struct PopoverButton: NSViewRepresentable { @Binding var isPresented: Bool @ViewBuilder var content: () -> Content func makeNSView(context: Context) -> NSButton { let button = NSButton(title: "Show", target: context.coordinator, action: #selector(Coordinator.showPopover)) return button } func updateNSView(_ nsView: NSButton, context: Context) { context.coordinator.isPresented = isPresented context.coordinator.content = AnyView(content()) if !isPresented { context.coordinator.popover?.close() } } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, NSPopoverDelegate { var parent: PopoverButton var popover: NSPopover? var isPresented: Bool = false var content: AnyView = AnyView(EmptyView()) init(_ parent: PopoverButton) { self.parent = parent } @objc func showPopover(_ sender: NSButton) { let popover = NSPopover() popover.contentViewController = NSHostingController(rootView: content) popover.behavior = .transient popover.delegate = self popover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .minY) self.popover = popover parent.isPresented = true } func popoverDidClose(_ notification: Notification) { parent.isPresented = false } } } ``` - Use NSViewRepresentable for custom views - Use Coordinator for delegate callbacks - Clean up resources in NSViewRepresentable - Use NSHostingView to embed SwiftUI in AppKit - Using AppKit when SwiftUI suffices - Forgetting to set acceptsFirstResponder for keyboard input - Not handling coordinate system (isFlipped) - Memory leaks from strong delegate references