在没有 XCode 或 NIB 的情况下使用 Swift 在 OSX 上显示 window

Display window on OSX using Swift without XCode or NIB

免责声明:我正在尝试以下练习,因为我认为它会很有启发性。我对如何完成它很感兴趣。所以请不要急于加入 "This is the wrong way to do it, you should never do it like this!"

使用我最喜欢的文本编辑器从命令行工作,我想构建一个显示 window.

的最小 Swift 程序

这是一个 GUI/Cococa 你好世界,如果你愿意的话。

本着同样的精神,我想避免NIB。

所以,没有 XCode,没有 NIB。

我愿意:

如果我能做到这两点,我就会脚踏实地,升级到 Xcode。

我尝试了以下方法:

window.swift

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    @IBOutlet weak var window: NSWindow!
    let newWindow = NSWindow(contentRect : NSScreen.mainScreen()!.frame
                             , styleMask : NSBorderlessWindowMask
                             ,   backing : NSBackingStoreType.Buffered 
                             ,     defer : false)
    func applicationDidFinishLaunching(aNotification: NSNotification) {
        // Insert code here to initialize your application
        newWindow.opaque = false
        newWindow.movableByWindowBackground = true
        newWindow.backgroundColor = NSColor.whiteColor()
        newWindow.makeKeyAndOrderFront(nil)
    }
    func applicationWillTerminate(aNotification: NSNotification) {
        // Insert code here to tear down your application
    }
}

但是,尝试从命令行运行失败:

pi@piBookAir.local ~ /Users/pi/dev/macdev:
 ⤐  swift window.swift 
window.swift:3:1: error: 'NSApplicationMain' attribute cannot be used in a 
                         module that contains top-level code
@NSApplicationMain
^
window.swift:1:1: note: top-level code defined in this source file
import Cocoa
^
 ✘

消除错误的正确方法是什么?

-parse-as-library 标志传递给 swiftc:

swiftc -parse-as-library window.swift

也就是说,当您这样做时,您最终会 运行 陷入第二个错误:

2015-06-10 14:17:47.093 window[11700:10854517] No Info.plist file in application bundle or no NSPrincipalClass in the Info.plist file, exiting

制作文件TestView.swift(像这样):

    import AppKit

    class TestView: NSView
    {
            override init(frame: NSRect)
            {
                    super.init(frame: frame)
            }

            required init?(coder: NSCoder)
            {
                    fatalError("init(coder:) has not been implemented")
            }

            var colorgreen = NSColor.greenColor()

            override func drawRect(rect: NSRect)
            {
                    colorgreen.setFill()
                    NSRectFill(self.bounds)

                    let h = rect.height
                    let w = rect.width
                    let color:NSColor = NSColor.yellowColor()

                    let drect = NSRect(x: (w * 0.25),y: (h * 0.25),width: (w * 0.5),height: (h * 0.5))
                    let bpath:NSBezierPath = NSBezierPath(rect: drect)

                    color.set()
                    bpath.stroke()

                    NSLog("drawRect has updated the view")
            }
    }

制作文件TestApplicationController.swift(像这样):

    import AppKit

    final class TestApplicationController: NSObject, NSApplicationDelegate
    {

            ///     Seems fine to create AppKit UI classes before `NSApplication` object
            ///     to be created starting OSX 10.10. (it was an error in OSX 10.9)
            let     window1 =       NSWindow()
            let     view1   =       TestView(frame: NSRect(x: 0, y: 0, width: 1000, height: 1000))

            func applicationDidFinishLaunching(aNotification: NSNotification)
            {
                    window1.setFrame(CGRect(x: 0, y: 0, width: 1000, height: 1000), display: true)
                    window1.contentView =   view1
                    window1.opaque      =   false
                    window1.center();
                    window1.makeKeyAndOrderFront(self)
            //      window1.backgroundColor = view1.colorgreen
            //      window1.displayIfNeeded()
            }

            func applicationWillTerminate(aNotification: NSNotification) {
                    // Insert code here to tear down your application
            }
    }

制作文件main.swift(像这样):

    //
    //  main.swift
    //  CollectionView
    //
    //  Created by Hoon H. on 2015/01/18.
    //  Copyright (c) 2015 Eonil. All rights reserved.
    //

    import AppKit

    let     app1            =       NSApplication.sharedApplication()
    let     con1            =       TestApplicationController()

    app1.delegate   =       con1
    app1.run()

最后一个文件不能重命名,main.swift显然是swift的特殊名称(否则无法编译示例)。

现在,输入这个(编译示例):

swiftc -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk TestView.swift TestApplicationController.swift main.swift

运行 输入代码:

./main

它显示了一个主 window(居中),其中有一个漂亮的绿色和一个黄色矩形)。 您可以通过输入 Control-C 来终止该应用程序。

请注意,这是一个 swift 编译而非解释器 运行,因此您有一个本机应用程序。

注意 -sdk 和 MacOSX10 的路径。11.sdk 是必不可少的(否则代码将无法编译)。

另请注意,此编译依赖于最新的 Xcode 发行版,因此请将 MacOSX10.11.sdk 更新为 MacOSX10.10.sdk 或 SDK 目录中的任何内容。

花了一段时间才找到这个...

this 代码从 objective-c 移植到 Swift,你会得到

import Cocoa

let nsapp = NSApplication.shared()
NSApp.setActivationPolicy(NSApplicationActivationPolicy.regular)
let menubar = NSMenu()
let appMenuItem = NSMenuItem()
menubar.addItem(appMenuItem)
NSApp.mainMenu = menubar
let appMenu = NSMenu()
let appName = ProcessInfo.processInfo.processName
let quitTitle = "Quit " + appName
let quitMenuItem = NSMenuItem.init(title:quitTitle,
  action:#selector(NSApplication.terminate),keyEquivalent:"q")
appMenu.addItem(quitMenuItem);
appMenuItem.submenu = appMenu;
let window = NSWindow.init(contentRect:NSMakeRect(0, 0, 200, 200),
    styleMask:NSTitledWindowMask,backing:NSBackingStoreType.buffered,defer:false)
window.cascadeTopLeft(from:NSMakePoint(20,20))
window.title = appName;
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps:true)
NSApp.run()

另存为minimal.swift,编译

swiftc minimal.swift -o minimal

和运行

./minimal

您将得到一个空的 window 和一个菜单栏,其中的菜单名称与应用程序类似,还有一个退出按钮。

为什么它能正常工作,我不知道。我是 Swift 和 Cocoa 编程的新手,但链接的网站解释了一点。

基于其他解决方案,我编写了一个最新版本的单文件应用程序。
要求:Swift 5.6,命令行工具,没有 XCode,没有 XIB,没有 Storyboard。
一个文件,一个class,运行它与swift app.swift

文件:app.swift

import AppKit

class App : NSObject, NSApplicationDelegate {
    let app = NSApplication.shared
    let name = ProcessInfo.processInfo.processName
    let status = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
    let window = NSWindow.init(
        contentRect: NSMakeRect(0, 0, 200, 200),
        styleMask: [.titled, .closable, .miniaturizable],
        backing: .buffered,
        defer: false
    )

    override init() {
        super.init()
        app.setActivationPolicy(.accessory)

        window.center()
        window.title = name
        window.hidesOnDeactivate = false
        window.isReleasedWhenClosed = false

        let statusMenu = newMenu()
        status.button?.title = ""
        status.menu = statusMenu

        let appMenu = newMenu()
        let sub = NSMenuItem()
        sub.submenu = appMenu
        app.mainMenu = NSMenu()
        app.mainMenu?.addItem(sub)
    }

    @IBAction func activate(_ sender:Any?) {
        app.setActivationPolicy(.regular)
        DispatchQueue.main.async { self.window.orderFrontRegardless() }
    }

    @IBAction func deactivate(_ sender:Any?) {
        app.setActivationPolicy(.accessory)
        DispatchQueue.main.async { self.window.orderOut(self) }
    }

    private func newMenu(title: String = "Menu") -> NSMenu {
        let menu = NSMenu(title: title)
        let q = NSMenuItem.init(title: "Quit",  action: #selector(app.terminate(_:)), keyEquivalent: "q")
        let w = NSMenuItem.init(title: "Close", action: #selector(deactivate(_:)),    keyEquivalent: "w")
        let o = NSMenuItem.init(title: "Open",  action: #selector(activate(_:)),      keyEquivalent: "o")
        for item in [o,w,q] { menu.addItem(item) }
        return menu
    }

    func applicationDidFinishLaunching(_ n: Notification) { }

    func applicationDidHide(_ n: Notification) {
        app.setActivationPolicy(.accessory)
        DispatchQueue.main.async { self.window.orderOut(self) }
    }
}

let app = NSApplication.shared
let delegate = App()
app.delegate = delegate
app.run()

上面的 64 行代码提供了以下功能并修复了以前的解决方案:

  • 应用程序菜单可见且可用
  • 状态菜单可以点击使用
  • 两个菜单都有可用的键绑定
  • 打开 window 时出现停靠项目:
    app.setActivationPolicy(.regular)
    
  • 停靠项目在 window 关闭时隐藏:
    app.setActivationPolicy(.accessory)
    
  • window 在关闭时保留并在打开时重复使用:
    window.hidesOnDeactivate = false
    window.isReleasedWhenClosed = false
    

在 M1 Pro 上测试。这是它的样子。