Swift scripts and macOS apps
Swift compiler 101, you possibly can create, construct and run a Swift file utilizing the swiftc
command. Think about the simplest Swift program that we will all think about in a most important.swift
file:
print("Hiya world!")
In Swift if we wish to print one thing, we do not even need to import the Basis framework, we will merely compile and run this piece of code by working the next:
swiftc most important.swift # compile most important.swift
chmod +x most important # add the executable permission
./most important # run the binary
The excellent news that we will take this one step additional by auto-invoking the Swift compiler underneath the hood with a shebang).
#! /usr/bin/swift
print("Hiya world!")
Now when you merely run the ./most important.swift
file it will print out the well-known “Hiya world!” textual content. 👋
Due to the program-loader mechanism and naturally the Swift interpreter we will skip an additional step and run our single-source Swift code as simple as an everyday shell script. The excellent news is that we will import all type of system frameworks which are a part of the Swift toolchain. With the assistance of Basis we will construct fairly helpful or utterly ineffective command line utilities.
#!/usr/bin/env swift
import Basis
import Dispatch
guard CommandLine.arguments.depend == 2 else {
fatalError("Invalid arguments")
}
let urlString = CommandLine.arguments[1]
guard let url = URL(string: urlString) else {
fatalError("Invalid URL")
}
struct Todo: Codable {
let title: String
let accomplished: Bool
}
let job = URLSession.shared.dataTask(with: url) { information, response, error in
if let error = error {
fatalError("Error: (error.localizedDescription)")
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
fatalError("Error: invalid HTTP response code")
}
guard let information = information else {
fatalError("Error: lacking response information")
}
do {
let decoder = JSONDecoder()
let todos = attempt decoder.decode([Todo].self, from: information)
print("Record of todos:")
print(todos.map { " - [" + ($0.completed ? "✅" : "❌") + "] ($0.title)" }.joined(separator: "n"))
exit(0)
}
catch {
fatalError("Error: (error.localizedDescription)")
}
}
job.resume()
dispatchMain()
In case you name this instance with a URL that may return a listing of todos it will print a pleasant listing of the gadgets.
./most important.swift https://jsonplaceholder.typicode.com/todos
Sure, you possibly can say that this script is totally ineffective, however for my part it is an incredible demo app, because it covers how you can test command line arguments (CommandLine.arguments
), it additionally reveals you how you can wait (dispatchMain
) for an async job, corresponding to a HTTP name via the community utilizing the URLSession API to complete and exit utilizing the correct methodology when one thing fails (fatalError
) or when you attain the tip of execution (exit(0)
). Just some traces of code, but it surely accommodates a lot data.
Have you ever observed the brand new shebang? When you’ve got a number of Swift variations put in in your system, you should use the env shebang to go along with the primary one which’s accessible in your PATH.
It is not simply Basis, however you possibly can import AppKit and even SwiftUI. Effectively, not underneath Linux in fact, since these frameworks are solely accessible for macOS plus you will have Xcode put in in your system, since some stuff in Swift the toolchain continues to be tied to the IDE, however why? 😢
Anyway, again to the subject, here is the boilerplate code for a macOS software Swift script that may be began from the Terminal with one easy ./most important.swift
command and nothing extra.
#!/usr/bin/env swift
import AppKit
import SwiftUI
@accessible(macOS 10.15, *)
struct HelloView: View {
var physique: some View {
Textual content("Hiya world!")
}
}
@accessible(macOS 10.15, *)
class WindowDelegate: NSObject, NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
NSApplication.shared.terminate(0)
}
}
@accessible(macOS 10.15, *)
class AppDelegate: NSObject, NSApplicationDelegate {
let window = NSWindow()
let windowDelegate = WindowDelegate()
func applicationDidFinishLaunching(_ notification: Notification) {
let appMenu = NSMenuItem()
appMenu.submenu = NSMenu()
appMenu.submenu?.addItem(NSMenuItem(title: "Give up", motion: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
let mainMenu = NSMenu(title: "My Swift Script")
mainMenu.addItem(appMenu)
NSApplication.shared.mainMenu = mainMenu
let measurement = CGSize(width: 480, peak: 270)
window.setContentSize(measurement)
window.styleMask = [.closable, .miniaturizable, .resizable, .titled]
window.delegate = windowDelegate
window.title = "My Swift Script"
let view = NSHostingView(rootView: HelloView())
view.body = CGRect(origin: .zero, measurement: measurement)
view.autoresizingMask = [.height, .width]
window.contentView!.addSubview(view)
window.middle()
window.makeKeyAndOrderFront(window)
NSApp.setActivationPolicy(.common)
NSApp.activate(ignoringOtherApps: true)
}
}
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.run()
Particular thanks goes to karwa for the authentic gist. Additionally if you’re into Storyboard-less macOS app improvement, it is best to positively check out this text by @kicsipixel. These assets helped me lots to place collectively what I wanted. I nonetheless needed to lengthen the gist with a correct menu setup and the activation coverage, however now this model acts like a real-world macOS software that works like a allure. There is just one difficulty right here… the script file is getting crowded. 🙈
Swift Bundle Supervisor and macOS apps
So, if we observe the identical logic, meaning we will construct an executable bundle that may invoke AppKit associated stuff utilizing the Swift Bundle Supervisor. Straightforward as a pie. 🥧
mkdir MyApp
cd MyApp
swift bundle init --type=executable
Now we will separate the parts into standalone information, we will additionally take away the provision checking, since we will add a platform constraint utilizing our Bundle.swift
manifest file. If you do not know a lot about how the Swift Bundle Supervisor works, please learn my SPM tutorial, or if you’re merely curious in regards to the construction of a Bundle.swift file, you possibly can learn my article in regards to the Swift Bundle manifest file. Let’s begin with the manifest updates.
import PackageDescription
let bundle = Bundle(
title: "MyApp",
platforms: [
.macOS(.v10_15)
],
dependencies: [
],
targets: [
.target(name: "MyApp", dependencies: []),
.testTarget(title: "MyAppTests", dependencies: ["MyApp"]),
]
)
Now we will place the HelloView struct into a brand new HelloView.swift file.
import SwiftUI
struct HelloView: View {
var physique: some View {
Textual content("Hiya world!")
}
}
The window delegate can have its personal place inside a WindowDelegate.swift file.
import AppKit
class WindowDelegate: NSObject, NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
NSApplication.shared.terminate(0)
}
}
We are able to apply the identical factor to the AppDelegate class.
import AppKit
import SwiftUI
class AppDelegate: NSObject, NSApplicationDelegate {
let window = NSWindow()
let windowDelegate = WindowDelegate()
func applicationDidFinishLaunching(_ notification: Notification) {
let appMenu = NSMenuItem()
appMenu.submenu = NSMenu()
appMenu.submenu?.addItem(NSMenuItem(title: "Give up", motion: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
let mainMenu = NSMenu(title: "My Swift Script")
mainMenu.addItem(appMenu)
NSApplication.shared.mainMenu = mainMenu
let measurement = CGSize(width: 480, peak: 270)
window.setContentSize(measurement)
window.styleMask = [.closable, .miniaturizable, .resizable, .titled]
window.delegate = windowDelegate
window.title = "My Swift Script"
let view = NSHostingView(rootView: HelloView())
view.body = CGRect(origin: .zero, measurement: measurement)
view.autoresizingMask = [.height, .width]
window.contentView!.addSubview(view)
window.middle()
window.makeKeyAndOrderFront(window)
NSApp.setActivationPolicy(.common)
NSApp.activate(ignoringOtherApps: true)
}
}
Lastly we will replace the primary.swift file and provoke all the things that must be finished.
import AppKit
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.run()
The excellent news is that this method works, so you possibly can develop, construct and run apps domestically, however sadly you possibly can’t submit them to the Mac App Retailer, because the last software bundle will not seem like an actual macOS bundle. The binary isn’t code signed, plus you may want an actual macOS goal in Xcode to submit the appliance. Then why hassle with this method?
Effectively, simply because it’s enjoyable and I may even keep away from utilizing Xcode with the assistance of SourceKit-LSP and a few Editor configuration. One of the best half is that SourceKit-LSP is now a part of Xcode, so you do not have to put in something particular, simply configure your favourite IDE and begin coding.
You can too bundle assets, since this function is accessible from Swift 5.3, and use them via the Bundle.module
variable if wanted. I already tried this, works fairly nicely, and it’s so a lot enjoyable to develop apps for the mac with out the additional overhead that Xcode comes with. 🥳