# 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