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

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, 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 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

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, 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:

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. 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:

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 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, 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, with specific boilerplate in references/representables.md and build commands in 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 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:

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →