命令行 Swift 脚本的最低限度可行的 GUI 是什么?

What is the minimally-viable GUI for command-line Swift scripts?

我的 Swift macOS 脚本需要一些辅助 GUI。它只需要一个文本输入字段和一个确定按钮。

我不想为了这个小弹出窗口走完所有臃肿的 Xcode 路线。但是,Apple 的文档让我失望,因为我的 NSWindow 没有捕获键盘输入。求助!

不感谢 Apple's documentation,我终于想出了从命令行启动简单 AppKit/Cocoa GUI Swift 应用程序 所需的魔法咒语 ‍♂️接受键盘输入没有Xcode!

这也是在 WKWebViews 中接受文本输入所必需的。

// main.swift // Dylan Sharhon // Tested on Catalina, Nov 2019
import AppKit // import Cocoa if you also need Foundation functionality

let app = NSApplication.shared
app.setActivationPolicy(.regular) // Magic to accept keyboard input and be docked!

let window = NSWindow.init(
  contentRect: NSRect(x: 300, y: 300, width: 200, height: 85),
  styleMask:   [
    NSWindow.StyleMask.titled     // Magic needed to accept keyboard input
  ],
  backing:     NSWindow.BackingStoreType.buffered,
  defer:       false
)
window.makeKeyAndOrderFront(nil)  // Magic needed to display the window

// Text input field
let text = NSTextField.init(string: "")
text.frame = NSRect(x: 10, y: 45, width: 180, height: 25)
window.contentView!.addSubview(text)

// Button
class Target {
  @objc func onClick () {         // Magic @objc needed for the button action
    print(text.stringValue)       // Goes to stdout
    exit(0)
  }
}
let target = Target()
let button = NSButton.init(
  title:  "OK",
  target: target,
  action: #selector(Target.onClick)
)
button.frame = NSRect(x:50, y:10, width:100, height:30)
window.contentView!.addSubview(button)

app.run()

致像我这样的菜鸟:这是整个应用程序,您可以使用 swift main.swift 运行 或使用 swiftc main.swift 编译它,将生成的(仅 40 KB)可执行文件重命名为您想要在菜单栏中显示的任何内容。

我写了一个小脚本,可以在 swift 中显示 window。目标是显示几个 shell 命令的输出(brew update & brew upgrade & brew cleanup & brew doctor)。如果您不是每天都执行这些命令,那么这些命令可能会花费大量时间,而且我已经厌倦了仅仅为了完成前 2 个命令就必须等待 10 分钟。

我本可以简单地启动一个 cron 作业或将 launchd 与 shell 脚本一起使用,但我希望能够检查命令的成功或失败,尤其是 brew doctor,以知道我是否需要执行一些操作来清理我机器上的自制程序安装。

所以我需要一个 window 来显示命令的错误和标准输出等等,我想生成它的二进制文件。

在 Google 和 Github 上稍作搜索后,我找到了 swift-sh,它允许导入 Github 存储库(以标准化的方式通过 Swift 包管理器)并在 swift 脚本中使用它并在需要时编译它;和 ShellOut,由同一个人开发,它允许从 swift 脚本执行 shell 命令,并在 swift 对象中收集命令的输出。

基本上,它应该有点 window 在滚动视图中带有文本视图,它显示了 shell 命令的输出,同时能够滚动它。

这里是脚本:


#!/usr/bin/swift sh


import AppKit
import Foundation
// importing ShellOut from GitHub repository
// The magic of swift-sh happens
import ShellOut // @JohnSundell

// Declare the Application context
let app = NSApplication.shared

// Create the delegate class responsible for the window and crontrol creation
class AppDelegate: NSObject, NSApplicationDelegate {
    var str: String? = ""

    // Construct the window
    let theWindow = NSWindow(contentRect: NSMakeRect(200, 200, 400, 200),
                          styleMask: [.titled, .closable, .miniaturizable, .resizable],
                          backing: .buffered,
                          defer: false,
                          screen: nil)

    var output: String? = ""

    // What happens once application context launched
    func applicationDidFinishLaunching(_ notification: Notification) {

        var str = ""

        // The shell commands and the collect of output
        do {
            str =  try shellOut(to: "brew", arguments: ["update"] )
            output = output! + str
        } catch {
            let error1 = error as! ShellOutError
            //print(error1.message)
            output = output! + error1.message
        }

        do {
            str = try shellOut(to: "brew", arguments: ["upgrade"] )
            output = output! + "\n" + str
            //print("step 2")
        } catch {
            let error2 = error as! ShellOutError
            //print(error2.message)
            output = output! + "\n" + error2.message
        }

        do {
            str = try shellOut(to: "brew", arguments: ["cleanup"] )
            output = output! + "\n" + str
            //print("step 3")
        } catch {
            let error3 = error as! ShellOutError
            //print(error3.message)
            output = output! + "\n" + error3.message
        }

        do {
            str = try shellOut(to: "brew", arguments: ["doctor"] )
            output = output! + "\n" + str
             //print("step 4")
        } catch {
            let error4 = error as! ShellOutError
            //print(error4.message)
            output = output! + "\n" + error4.message
        }

        // Controls placement and content goes here
        // ScrollView...
        var theScrollview = NSScrollView(frame: theWindow.contentView!.bounds)
        var contentSize = theScrollview.contentSize
        theScrollview.borderType = .noBorder
        theScrollview.hasVerticalScroller = true
        theScrollview.hasHorizontalScroller = false
        theScrollview.autoresizingMask = NSView.AutoresizingMask(rawValue: NSView.AutoresizingMask.width.rawValue | NSView.AutoresizingMask.height.rawValue | NSView.AutoresizingMask.minYMargin.rawValue | NSView.AutoresizingMask.minYMargin.rawValue)

        // TextView...
        var theTextView = NSTextView(frame: NSMakeRect(0, 0, contentSize.width, contentSize.height))
        theTextView.minSize = NSMakeSize(0.0, contentSize.height)
        theTextView.maxSize = NSMakeSize(CGFloat.greatestFiniteMagnitude, CGFloat.greatestFiniteMagnitude)
        theTextView.isVerticallyResizable = true
        theTextView.isHorizontallyResizable = false
        theTextView.autoresizingMask = NSView.AutoresizingMask(rawValue: NSView.AutoresizingMask.width.rawValue | NSView.AutoresizingMask.height.rawValue | NSView.AutoresizingMask.minYMargin.rawValue | NSView.AutoresizingMask.minYMargin.rawValue)
        theTextView.textContainer?.containerSize = NSMakeSize(contentSize.width, CGFloat.greatestFiniteMagnitude)
        theTextView.backgroundColor = .white
        theTextView.textContainer?.widthTracksTextView = true
        theTextView.textStorage?.append(NSAttributedString(string: output!))

        theScrollview.documentView = theTextView

        theWindow.contentView = theScrollview
        theWindow.makeKeyAndOrderFront(nil)
        theWindow.makeFirstResponder(theTextView)
    }

    // What happens when we click the close button of the window
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
      return true;
    }
}

// Instantiation of the application delegate class
// and launching
let delegate = AppDelegate()
    app.delegate = delegate
    app.run()