Search code examples
swiftmacosswiftuiswiftpm

Make SwiftUI app appear in the macOS Dock


I'm a complete noob at macOS UI programming. Last time I did that was sometime in the 20th century using Visual Mac Standard Basic or REALbasic…

I'm trying to build a graphical UI to interact with some algorithms written in Swift using SwiftUI. I don't intend on distributing a runnable application bundle, instead the GUI should be launched from the command line using swift run or similar.

I've managed to get a window with my UI shown on the screen but it doesn't really behave. It doesn't get focus when running the executable from the command line and clicking the window makes it look like the frontmost window but the window from Terminal.app still has the focus and receives keystrokes. There's no app icon in the Dock (even a generic one) and I can't switch to the application using Command-Tab.

What am I missing to get the usual behaviour from the application in this regard?

I'm using macOS 10.15.7 and Xcode 12.4. I want the application to be build using the Swift Package Manager from the command line. Below are the files from the minimal project I'm woking with:

Package.swift
// swift-tools-version:5.3
import PackageDescription

let linkerFlags = [
    "-Xlinker", "-sectcreate",
    "-Xlinker", "__TEXT",
    "-Xlinker", "__info_plist",
    "-Xlinker", "Resources/Info.plist"]

let package = Package(
    name: "ConnectedRegions",
    platforms: [.macOS(.v10_15)],
    products: [
        .executable(name: "ConnectedRegions", targets: ["ConnectedRegions"])],
    targets: [
        .target(
            name: "ConnectedRegions",
            linkerSettings: [.unsafeFlags(linkerFlags)])])
Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSPrincipalClass</key>
    <string>NSApplication</string>
</dict>
</plist>
main.swift
import Cocoa
import SwiftUI

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 200, height: 200),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered,
            defer: false)
        
        window.center()
        window.contentView = NSHostingView(rootView: Text("Hello").padding(50))
        window.makeKeyAndOrderFront(nil)
    }
}

let delegate = AppDelegate()
NSApplication.shared.delegate = delegate

_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

Solution

  • With the new App protocol from SwiftUI in macOS 11, which provides an entry point, this is quite easy to achieve. No Info.plist is necessary to get a default menu and Dock icon:

    import SwiftUI
    
    @main
    struct MyApp: App {
        @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        var body: some Scene {
            WindowGroup {
                VStack {
                    Button("Done") { exit(0) }
                }
                .padding(100)
            }
        }
    }
    
    class AppDelegate: NSObject, NSApplicationDelegate {
        func applicationDidFinishLaunching(_ notification: Notification) {
            NSApplication.shared.setActivationPolicy(.regular)
            NSApplication.shared.activate(ignoringOtherApps: true)
        }
    }