# How to Use SwiftUI with AppKit for Complex macOS UIs: The Palmier Pro Pattern

> Master SwiftUI with AppKit for complex macOS UIs using NSViewRepresentable. Build custom views like drag-and-drop and video previews while keeping SwiftUI's state management.

- Repository: [Palmier/palmier-pro](https://github.com/palmier-io/palmier-pro)
- Tags: how-to-guide
- Published: 2026-06-24

---

**Use the `NSViewRepresentable` protocol to wrap AppKit `NSView` subclasses in SwiftUI views, enabling complex macOS interfaces like custom scroll views, drag-and-drop zones, and high-performance video previews while maintaining SwiftUI's reactive state management.**

When building sophisticated macOS applications, you often encounter interface requirements that exceed SwiftUI's native capabilities. The Palmier Pro repository demonstrates a production-ready architecture for integrating SwiftUI with AppKit, allowing developers to leverage mature macOS frameworks while preserving SwiftUI's declarative syntax and data flow.

## Why Bridge SwiftUI and AppKit?

Complex macOS UIs frequently require fine-grained control unavailable in pure SwiftUI. AppKit provides mature view hierarchies including `NSScrollView`, `NSHostingView`, and `AVPlayerLayer` that can be tailored for performance or native macOS behavior. Once wrapped using `NSViewRepresentable`, these AppKit views participate fully in SwiftUI's layout system and can access `@Environment` values and `@Binding` properties.

The bridging pattern also enables the **Coordinator** object within `NSViewRepresentable` to handle delegate callbacks, notifications, and gesture handling while forwarding changes back to SwiftUI state. This separation of concerns keeps low-level drawing and event handling in AppKit while SwiftUI manages high-level layout and application state.

## The NSViewRepresentable Protocol Structure

The core of this integration is the `NSViewRepresentable` protocol, which requires three main methods: `makeNSView(context:)` to create the native view, `updateNSView(_:context:)` to sync SwiftUI state changes, and optionally `dismantleNSView(_:coordinator:)` for cleanup.

### Basic Implementation Skeleton

```swift
struct MyWrapper: NSViewRepresentable {
    @Binding var isActive: Bool
    let onDrop: ([URL]) -> Void

    func makeNSView(context: Context) -> MyNSView {
        let view = MyNSView()
        view.onDrop = onDrop
        view.onTargetChanged = { isActive = $0 }
        return view
    }

    func updateNSView(_ nsView: MyNSView, context: Context) {
        // Sync view with SwiftUI state
    }

    static func dismantleNSView(_ nsView: MyNSView, coordinator: Coordinator) {
        // Cleanup resources
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    final class Coordinator {
        // Holds delegates and notification observers
    }
}

```

The `makeNSView` method instantiates the native view and attaches closures that modify SwiftUI bindings. The `updateNSView` method reacts to state changes, while the coordinator bridges AppKit delegate patterns back to SwiftUI.

## Real-World Implementation Examples from Palmier Pro

Palmier Pro implements several sophisticated `NSViewRepresentable` wrappers that demonstrate different aspects of the SwiftUI-AppKit bridge.

### Drag-and-Drop with MediaPanelDropArea

For custom drag-and-drop targets, `MediaPanelDropArea` wraps `DropHostingView` (an `NSHostingView` subclass) to forward AppKit drag events to SwiftUI bindings.

```swift
struct MediaPanelDropArea<Content: View>: NSViewRepresentable {
    @Binding var isTargeted: Bool
    let onDrop: (_ urls: [URL]) -> Void
    @ViewBuilder let content: () -> Content

    func makeNSView(context: Context) -> DropHostingView<Content> {
        let view = DropHostingView(rootView: content())
        view.onTargetChanged = { isTargeted = $0 }
        view.onDrop = onDrop
        return view
    }

    func updateNSView(_ nsView: DropHostingView<Content>, context: Context) {
        nsView.rootView = content()
        nsView.onTargetChanged = { isTargeted = $0 }
        nsView.onDrop = onDrop
    }
}

```

In [`Sources/PalmierPro/MediaPanel/MediaTab/MediaPanelDropArea.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/MediaPanel/MediaTab/MediaPanelDropArea.swift), the wrapper forwards `draggingEntered`, `draggingExited`, and `performDragOperation` from the AppKit view to the SwiftUI `isTargeted` binding and `onDrop` closure.

### Scrollable Timelines with TimelineContainerView

For complex scrolling interfaces, `TimelineContainerView` creates an `NSScrollView` that hosts a custom `TimelineView`. The coordinator observes `NSView.boundsDidChangeNotification` and `NSView.frameDidChangeNotification` to synchronize SwiftUI graphics with scroll position.

```swift
@MainActor @objc func scrollViewBoundsChanged(_ notification: Notification) {
    timelineView?.needsDisplay = true
    timelineView?.updatePlayheadLayer()
    if let scrollY = scrollView?.contentView.bounds.origin.y {
        headerView?.setBoundsOrigin(NSPoint(x: 0, y: scrollY))
        headerView?.needsDisplay = true
    }
}

```

As implemented in [`Sources/PalmierPro/Timeline/TimelineContainerView.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Timeline/TimelineContainerView.swift), the coordinator stores references to native sub-views and caches the latest `RenderState` to determine whether redraws are required. This pattern enables independent scrolling of header and content layers while maintaining smooth 60fps performance.

### Video Previews with PreviewView

For high-performance video rendering, `PreviewView` wraps `PreviewNSView`, which owns an `AVPlayerLayer`. The implementation exposes command-scroll zoom functionality through closures that bridge AppKit events to SwiftUI state.

```swift
view.onCmdScroll = { [weak editor] deltaY, pointTopDown, viewSize in
    guard let editor = editor else { return }
    // Compute new zoom and offset values
    editor.canvasOffset = newOffset
    editor.canvasZoom = newZoom
}

```

According to [`Sources/PalmierPro/Preview/PreviewView.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Preview/PreviewView.swift), the coordinator holds a reference to the `VideoEngine`, ensuring the preview stays alive while SwiftUI owns the wrapper. This approach keeps heavy CoreAnimation layers in AppKit while SwiftUI manages the surrounding layout and toolbars.

## Performance and Architecture Benefits

Separating concerns between SwiftUI and AppKit yields significant architectural advantages. AppKit handles low-level drawing, scrolling inertia, and drag-and-drop operations, while SwiftUI declares the surrounding layout and manages application state. This separation allows heavy graphics components like video layers or custom `CALayer` trees to avoid SwiftUI's view recomputation overhead for every frame.

Wrapped views remain fully composable within SwiftUI hierarchies. You can apply standard modifiers like `.padding()`, `.background()`, or custom `AppTheme` values defined in [`Sources/PalmierPro/UI/AppTheme.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/UI/AppTheme.swift) to wrapped components. The top-level `EditorView` in [`Sources/PalmierPro/Editor/EditorView.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Editor/EditorView.swift) demonstrates how these wrapped components integrate seamlessly with pure SwiftUI views.

## Summary

- **Use `NSViewRepresentable`** to bridge AppKit views into SwiftUI while maintaining access to bindings and environment objects.
- **Implement a Coordinator** to handle AppKit delegate callbacks and notifications, forwarding events to SwiftUI state.
- **Reference production patterns** from [`TimelineContainerView.swift`](https://github.com/palmier-io/palmier-pro/blob/main/TimelineContainerView.swift), [`MediaPanelDropArea.swift`](https://github.com/palmier-io/palmier-pro/blob/main/MediaPanelDropArea.swift), and [`PreviewView.swift`](https://github.com/palmier-io/palmier-pro/blob/main/PreviewView.swift) for scroll views, drag-and-drop, and video rendering.
- **Keep heavy graphics in AppKit** to avoid SwiftUI performance overhead while using SwiftUI for layout and state management.
- **Apply SwiftUI modifiers** to wrapped views just like native SwiftUI components, ensuring consistent theming through shared resources like [`AppTheme.swift`](https://github.com/palmier-io/palmier-pro/blob/main/AppTheme.swift).

## Frequently Asked Questions

### How do I handle state synchronization between AppKit and SwiftUI?

Use `@Binding` properties in your `NSViewRepresentable` implementation and update them from within the coordinator or view closures. The `updateNSView` method receives the latest SwiftUI state whenever bindings change, allowing you to sync native view properties. For AppKit-to-SwiftUI communication, capture bindings in closures assigned to the native view during `makeNSView`, or use the coordinator to observe notifications and update published properties.

### Can I use SwiftUI views inside my wrapped AppKit view?

Yes, by using `NSHostingView` as your container or as a subview within your custom `NSView` subclass. `MediaPanelDropArea` demonstrates this by wrapping `DropHostingView` (an `NSHostingView` subclass), which allows SwiftUI content to reside inside the AppKit drag-and-drop target while still receiving AppKit events.

### What is the performance impact of using NSViewRepresentable?

When implemented correctly, the performance impact is minimal because AppKit handles the heavy rendering operations directly. The pattern actually improves performance for complex UIs by keeping CoreAnimation layers, video buffers, and custom drawing code in optimized AppKit views while avoiding SwiftUI's reconciliation overhead for frames that don't change. Ensure you implement `updateNSView` efficiently to avoid unnecessary view recreation.

### How do I handle cleanup when the view disappears?

Implement the static `dismantleNSView(_:coordinator:)` method in your `NSViewRepresentable` conformance. This method receives the view and coordinator when SwiftUI removes the view from the hierarchy, allowing you to invalidate timers, remove notification observers, release `VideoEngine` references, or perform other resource cleanup. The coordinator persists for the lifetime of the view, making it ideal for managing resources that must survive view updates but require cleanup on removal.