Build a SwiftUI macOS menu bar app with drag and drop

This article is a practical macOS SwiftUI walkthrough. The goal is to build a menu bar app, keep the SwiftUI app lifecycle, bridge in the delegate hooks you still need, accept dropped URLs on the status item window, and update the popover view dynamically when new content arrives.

macOS menu bar app demo showing drag and drop into a SwiftUI popover

This pattern matters because SwiftUI handles the interface well, but macOS menu bar apps still need some AppKit lifecycle control.

This article is built around three concrete goals: use NSApplicationDelegateAdaptor inside a SwiftUI app, accept drag and drop on a menu bar app, and update the SwiftUI view when a dropped item arrives. That makes it a good bridge article between pure SwiftUI structure and the older delegate-driven parts of macOS.

In practice, the architecture is simple. SwiftUI still owns the app entry point and the popover content view. The delegate owns the status item, the popover wiring, and the drag-and-drop callbacks. A shared observable object carries the dropped URL back into SwiftUI.

What You Learn How to host an AppDelegate inside the SwiftUI lifecycle, register drag types on the menu bar window, and push dropped data back into a SwiftUI view.

Start with a normal SwiftUI app, then bridge the old delegate API back in with @NSApplicationDelegateAdaptor.

In newer Xcode templates, a SwiftUI app no longer starts with an AppDelegate.swift file. That is fine until you need lifecycle hooks like applicationDidFinishLaunching, which are especially useful for creating menu bar items only after launch.

This article solves that by adding an adaptor property directly to the SwiftUI app entry point:

@NSApplicationDelegateAdaptor(MenuBar_Drag_and_Drop_DemoApp_AppDelegate.self) var appDelegate

Then the app itself stays mostly standard SwiftUI:

@main
struct MenuBar_Drag_and_Drop_DemoApp: App {
    @NSApplicationDelegateAdaptor(MenuBar_Drag_and_Drop_DemoApp_AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Because this is macOS, the article uses NSApplicationDelegateAdaptor. The iOS equivalent would be UIApplicationDelegateAdaptor.

class MenuBar_Drag_and_Drop_DemoApp_AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {

    }
}

The delegate creates the status item, hosts a SwiftUI view inside an NSPopover, and toggles that popover from the menu bar button.

The next step is the actual menu bar app shell. The delegate stores an NSPopover and NSStatusItem, creates the SwiftUI ContentView, wraps it in an NSHostingController, and uses that as the popover content.

class MenuBar_Drag_and_Drop_DemoApp_AppDelegate: NSObject, NSApplicationDelegate {
    var popover: NSPopover!
    var statusBarItem: NSStatusItem!

    func applicationDidFinishLaunching(_ notification: Notification) {
        let contentView = ContentView()

        let popover = NSPopover()
        popover.behavior = .transient
        popover.contentViewController = NSHostingController(rootView: contentView)
        self.popover = popover

        self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
        guard let button = self.statusBarItem.button else { return }
        button.image = NSImage(systemSymbolName: "checkmark.circle.fill", accessibilityDescription: nil)
        button.action = #selector(showHidePopover(_:))
    }

    @objc func showHidePopover(_ sender: AnyObject?) {
        guard let button = self.statusBarItem.button else { return }
        if self.popover.isShown {
            self.popover.performClose(sender)
        } else {
            self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
            self.popover.contentViewController?.view.window?.becomeKey()
        }
    }
}

That gets you the menu bar button, the popover, and a SwiftUI content surface without leaving the SwiftUI app lifecycle.

Menu bar app popover showing a dropped URL
The popover is still a SwiftUI view. AppKit only manages the menu bar entry point and hosting container around it.

Drag and drop starts at the status item window, so the delegate registers accepted types there and adopts the drag destination protocols.

In this setup, the drop target is effectively the menu bar button's window. The article registers supported types there and assigns the delegate so it can receive the drag callbacks.

func applicationDidFinishLaunching(_ notification: Notification) {
    // ... existing setup ...
    button.window?.registerForDraggedTypes([.URL, .string])
    button.window?.delegate = self
}

The delegate class is then extended to conform to NSPopoverDelegate, NSWindowDelegate, and NSDraggingDestination. The key method is performDragOperation, which reads the first dropped NSURL from the pasteboard.

extension MenuBar_Drag_and_Drop_DemoApp_AppDelegate: NSPopoverDelegate, NSWindowDelegate, NSDraggingDestination {
    func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
        return .link
    }

    func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
        return sender.draggingSourceOperationMask
    }

    func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
        if let firstObject = sender.draggingPasteboard.readObjects(forClasses: [NSURL.self])?.first {
            if let droppedItem = firstObject as? NSURL {
                print(droppedItem.absoluteString ?? "")
                self.showHidePopover(nil)
            }
            return true
        }
        return false
    }
}

At that point, the menu bar app already accepts dropped URLs. The remaining problem is how to get that URL back into the SwiftUI view.

A shared ObservableObject is the bridge between the AppKit delegate world and the SwiftUI view tree.

This article uses a singleton-style helper object so both the delegate and the SwiftUI view can access the same state. The actual data is just the dropped URL string, marked with @Published so SwiftUI redraws when it changes.

class MenuBar_DroppedDataHelper: NSObject, ObservableObject {
    static let shared = MenuBar_DroppedDataHelper()
    @Published var droppedURLStr: String?
}

The delegate stores a reference to that shared object:

class MenuBar_Drag_and_Drop_DemoApp_AppDelegate: NSObject, NSApplicationDelegate {
    var popover: NSPopover!
    var statusBarItem: NSStatusItem!

    @ObservedObject var dataHelper = MenuBar_DroppedDataHelper.shared
}

Then the drag handler updates the shared value when a dropped URL is found:

if let firstObject = sender.draggingPasteboard.readObjects(forClasses: [NSURL.self])?.first {
    if let droppedItem = firstObject as? NSURL {
        print(droppedItem.absoluteString ?? "")
        self.dataHelper.droppedURLStr = droppedItem.absoluteString
        self.showHidePopover(nil)
    }
    return true
}

Finally, the SwiftUI view observes the same shared object and renders the URL:

struct ContentView: View {
    @ObservedObject var dataHelper = MenuBar_DroppedDataHelper.shared

    var body: some View {
        VStack(alignment: .center) {
            Image(systemName: "checkmark.bubble.fill")
                .padding(.bottom)

            Text("Here's the dropped URL:")
            Text(dataHelper.droppedURLStr ?? "")
        }
        .padding()
    }
}

With that in place, dropping a link on the menu bar item updates the popover content automatically.

The important pattern here is not just drag and drop. It is mixing SwiftUI ownership of the UI with AppKit ownership of the system integration points.

Menu bar apps, popovers, delegate callbacks, and drag-and-drop targets still live close to AppKit. SwiftUI is a great layer for the content, but the article shows how to keep the SwiftUI app structure while still reaching into the delegate lifecycle where macOS expects it.

That is why this example holds up. It gives you a complete path from app launch to menu bar item to dropped data to reactive SwiftUI updates, without forcing you back into an all-AppKit application architecture.