# How to Configure macOS AppKit SwiftUI Plugin Workflows: A Complete Guide

> Learn to configure macOS AppKit SwiftUI plugin workflows. Scaffold a SwiftUI app, bridge AppKit controls, and manage state effectively without leaking AppKit types. Get the complete guide.

- Repository: [OpenAI/plugins](https://github.com/openai/plugins)
- Tags: how-to-guide
- Published: 2026-06-07

---

**Configure macOS AppKit SwiftUI plugin workflows by scaffolding a SwiftUI-first application and wrapping specific AppKit controls in thin `NSViewRepresentable` bridges with coordinators that mediate state changes without leaking AppKit types into your declarative view hierarchy.**

The OpenAI Plugins repository defines a strict architectural pattern to configure macOS AppKit SwiftUI plugin workflows that maximizes code reuse and maintainability. By keeping the bulk of the UI in SwiftUI and isolating AppKit dependencies to narrow, well-defined bridges, you maintain declarative state management while accessing platform-specific capabilities that SwiftUI does not yet expose.

## The Three-Phase Workflow

The workflow to configure macOS AppKit SwiftUI plugin workflows consists of three distinct phases: scaffolding the SwiftUI shell, identifying where AppKit interop is necessary, and implementing a thin bridge with proper state ownership.

### Phase 1: Scaffold the SwiftUI Shell

Start by creating a pure SwiftUI application structure. According to [`plugins/build-macos-apps/skills/appkit-interop/references/app-wiring.md`](https://github.com/openai/plugins/blob/main/plugins/build-macos-apps/skills/appkit-interop/references/app-wiring.md), this involves:

- Creating a `@main` **App** struct conforming to the SwiftUI `App` protocol
- Defining an `AppTab` enum for navigation state
- Wrapping the root view in a `NavigationStack` or `TabView` scaffold
- Injecting environment objects for global state

This establishes the **SwiftUI-first** foundation, ensuring that AppKit types remain confined to specific integration points rather than spreading throughout the codebase.

### Phase 2: Determine the AppKit Interop Pattern

Before writing bridge code, consult the decision matrix documented in [`plugins/build-macos-apps/skills/appkit-interop/SKILL.md`](https://github.com/openai/plugins/blob/main/plugins/build-macos-apps/skills/appkit-interop/SKILL.md) under the *Good fits for AppKit* section. Choose the appropriate bridge type based on the required functionality:

- **`NSViewRepresentable`** – For wrapping individual AppKit views like `NSTextView`, `NSScrollView`, or custom `NSView` subclasses that require complex drawing or text handling
- **`NSViewControllerRepresentable`** – When you need to embed a full `NSViewController` with its own lifecycle, such as a complex preference pane or document-based interface
- **Direct `NSWindow` or `NSPanel` access** – For menus, floating panels, or responder-chain handling that SwiftUI cannot replicate, as detailed in [`plugins/build-macos-apps/skills/appkit-interop/references/window-panels.md`](https://github.com/openai/plugins/blob/main/plugins/build-macos-apps/skills/appkit-interop/references/window-panels.md)

This selection prevents the "AppKit escape hatches" anti-pattern by enforcing that you only bridge when SwiftUI APIs are genuinely insufficient.

### Phase 3: Implement the Representable Bridge

Using the boilerplate from [`plugins/build-macos-apps/skills/appkit-interop/references/representables.md`](https://github.com/openai/plugins/blob/main/plugins/build-macos-apps/skills/appkit-interop/references/representables.md), create a thin wrapper that contains three essential components:

1. **The Representable struct** conforming to `NSViewRepresentable` or `NSViewControllerRepresentable`
2. **A Coordinator class** that acts as the delegate for AppKit callbacks and mediates state changes back to SwiftUI
3. **An `updateNSView` or `updateUIViewController` method** that performs diffing to avoid infinite loops by only pushing changes when the data actually differs

The coordinator must own `@Binding` properties to ensure state flows bidirectionally: from SwiftUI into the AppKit view via the update method, and from AppKit back to SwiftUI via delegate callbacks.

## macOS AppKit SwiftUI Bridge Implementation Example

The following example from the OpenAI Plugins repository demonstrates wrapping an `NSTextView` inside SwiftUI while maintaining clean state ownership:

```swift
import SwiftUI
import AppKit

// A thin NSViewRepresentable wrapper for an AppKit text view.
struct LegacyTextView: NSViewRepresentable {
    @Binding var text: String          // SwiftUI <-> AppKit binding

    func makeCoordinator() -> Coordinator {
        Coordinator(text: $text)
    }

    func makeNSView(context: Context) -> NSScrollView {
        let scrollView = NSScrollView()
        let textView = NSTextView()
        textView.delegate = context.coordinator           // delegate <-> coordinator
        scrollView.documentView = textView
        return scrollView
    }

    func updateNSView(_ nsView: NSScrollView, context: Context) {
        guard let textView = nsView.documentView as? NSTextView else { return }
        // Avoid infinite loops: only push when the strings differ
        if textView.string != text {
            textView.string = text
        }
    }

    // Coordinator mediates AppKit -> SwiftUI events
    final class Coordinator: NSObject, NSTextViewDelegate {
        @Binding var text: String

        init(text: Binding<String>) { _text = text }

        func textDidChange(_ notification: Notification) {
            guard let textView = notification.object as? NSTextView else { return }
            text = textView.string                // updates SwiftUI @State
        }
    }
}

// Using the representable inside a SwiftUI view
struct ContentView: View {
    @State private var message = "Hello, AppKit!"

    var body: some View {
        VStack {
            LegacyTextView(text: $message)          // bridge inserted here
                .frame(height: 120)
                .border(Color.gray)

            Text("SwiftUI sees: \(message)")
                .padding()
        }
        .padding()
    }
}

```

In this implementation, the `LegacyTextView` struct lives entirely within the representable pattern described in [`plugins/build-macos-apps/skills/appkit-interop/references/representables.md`](https://github.com/openai/plugins/blob/main/plugins/build-macos-apps/skills/appkit-interop/references/representables.md). The `Coordinator` class conforms to `NSTextViewDelegate` to capture user input, while the `updateNSView` method prevents feedback loops by checking `textView.string != text` before assignment.

## Build and Test the Plugin

After implementing the bridge, build and launch the macOS application using the commands specified in [`plugins/build-macos-apps/skills/build-run-debug/SKILL.md`](https://github.com/openai/plugins/blob/main/plugins/build-macos-apps/skills/build-run-debug/SKILL.md):

```bash
swift build
open .build/debug/YourApp.app

```

Verify that:
- The AppKit view renders correctly within the SwiftUI layout
- State changes propagate bidirectionally without causing duplicate updates or infinite loops
- Dark mode transitions and accessibility features work as expected

If the bridge introduces performance issues such as heavy redraws, consult [`plugins/build-macos-apps/skills/swiftui-ui-patterns/references/performance.md`](https://github.com/openai/plugins/blob/main/plugins/build-macos-apps/skills/swiftui-ui-patterns/references/performance.md) for identity and lazy-container guidelines.

## Summary

- **SwiftUI-first architecture** keeps the majority of UI code declarative and portable, with AppKit isolated to specific representable wrappers.
- **Three-phase workflow** involves scaffolding the app shell, selecting the appropriate interop pattern from the decision matrix in [`appkit-interop/SKILL.md`](https://github.com/openai/plugins/blob/main/appkit-interop/SKILL.md), and implementing a thin bridge.
- **Coordinator pattern** is essential for mediating delegate callbacks from AppKit back to SwiftUI `@State` or `@Binding` properties.
- **Diffing in `updateNSView`** prevents infinite loops by only pushing data when values actually change.
- **File references**: The primary documentation resides in [`plugins/build-macos-apps/skills/appkit-interop/SKILL.md`](https://github.com/openai/plugins/blob/main/plugins/build-macos-apps/skills/appkit-interop/SKILL.md), with specific boilerplate in [`references/representables.md`](https://github.com/openai/plugins/blob/main/references/representables.md) and build commands in [`build-run-debug/SKILL.md`](https://github.com/openai/plugins/blob/main/build-run-debug/SKILL.md).

## Frequently Asked Questions

### What is the difference between NSViewRepresentable and NSViewControllerRepresentable?

**`NSViewRepresentable`** wraps a single `NSView` instance for narrow integration tasks like embedding a complex text editor or custom drawing view. **`NSViewControllerRepresentable`** wraps an entire `NSViewController`, making it suitable for scenarios requiring full view controller lifecycle management, such as preference panes or document windows that rely on `NSDocument` architecture. According to the OpenAI Plugins source code, choose the former for view-level bridges and the latter only when controller-level features are mandatory.

### How do I prevent infinite loops when binding AppKit delegate callbacks to SwiftUI state?

Implement defensive checks in the `updateNSView` or `updateUIViewController` method to verify that the new value differs from the current AppKit view's value before assignment, as shown in the `if textView.string != text` guard clause. The coordinator should update the SwiftUI binding in the delegate callback, but the update method must skip redundant sets to break the feedback cycle.

### Where should AppKit-specific state live in a mixed SwiftUI/AppKit plugin?

AppKit-specific state should remain confined to the **coordinator** or the AppKit view itself, while business logic and UI state belong to SwiftUI `@State`, `@Binding`, or `@Observable` objects. Follow the state-ownership matrix documented in [`plugins/build-macos-apps/skills/swiftui-ui-patterns/SKILL.md`](https://github.com/openai/plugins/blob/main/plugins/build-macos-apps/skills/swiftui-ui-patterns/SKILL.md) to ensure the bridge only receives binding values and does not expose AppKit types to the rest of the SwiftUI view hierarchy.

### How do I build and run the macOS plugin from the command line?

Use the standard Swift Package Manager build command followed by opening the generated app bundle, as documented in [`plugins/build-macos-apps/skills/build-run-debug/SKILL.md`](https://github.com/openai/plugins/blob/main/plugins/build-macos-apps/skills/build-run-debug/SKILL.md):

```bash
swift build && open .build/debug/YourApp.app

```

This ensures the GUI app launches with proper macOS sandboxing and entitlements applied, rather than running as a headless process.