Reputation: 547
I need a little helper GUI for my Swift script on macOS. It just needs a text entry field and an OK button.
I don't want to go the whole bloated Xcode route just for this little popup. However, Apple's documentation fails me because keyboard input isn't captured by my NSWindow. Help!
Upvotes: 8
Views: 1574
Reputation: 326
I wrote a little script which is able to show a window in swift. The goal was to show the output of several shell commands (brew update
& brew upgrade
& brew cleanup
& brew doctor
). These commands can take significant amount of time if you do not perform them everyday and I was tired of having to wait sometime 10 minutes just for the 2 first commands to complete.
I could have simply launched a cron job or used launchd with a shell script, but I wanted to be able to check the success or failure of the commands, especially the brew doctor
, to know if I needed to perform some action to clean my homebrew install on my machine.
So I needed a window to show up with the error and standard output of the commands and more, I wanted to produce a binary of it.
After searching a bit on Google and Github, I found swift-sh, which allow to import Github repository (in a standardized way through Swift Package Manager) and use it in a swift script and compile it if needed ; and ShellOut, by the same guy, which allows to perform shell commands from a swift script and collects the output of the command in an swift object.
Basically, it should have been a little window with a textview in a scrollview, which showed the output of the shell commands while being able to scroll it.
Here the Script :
#!/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()
Upvotes: 2
Reputation: 547
No thanks to Apple's documentation, I finally figured out the magical incantations 🧙♂️ needed to launch a simple AppKit/Cocoa GUI from a command-line Swift app that accepts keyboard input. Without Xcode!
This is also needed to accept text input in 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()
To noobs like me: This is the entire app and you can run it with swift main.swift
or compile it with swiftc main.swift
and rename the resulting (merely 40 KB) executable to whatever you want to be in the menubar.
Upvotes: 11