Swift scripts and macOS apps
Swift compiler 101, you may 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 major.swift
file:
print("Hiya world!")
In Swift if we wish to print one thing, we do not even must import the Basis framework, we will merely compile and run this piece of code by operating the next:
swiftc major.swift # compile major.swift
chmod +x major # add the executable permission
./major # run the binary
The excellent news that we will take this one step additional by auto-invoking the Swift compiler below the hood with a shebang.
#! /usr/bin/swift
print("Hiya world!")
Now when you merely run the ./major.swift
file it’s going to print out the well-known “Hiya world!” textual content. 👋
Because of 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 a daily shell script. The excellent news is that we will import all form of system frameworks which might be 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("Listing of todos:")
print(todos.map " - [" + ($0.completed ? "✅" : "❌") + "] ($0.title)" .joined(separator: "n"))
exit(0)
catch
fatalError("Error: (error.localizedDescription)")
job.resume()
dispatchMain()
Should you name this instance with a URL that may return a listing of todos it’s going to print a pleasant listing of the objects.
./major.swift https://jsonplaceholder.typicode.com/todos
Sure, you may say that this script is totally ineffective, however in my view it is a tremendous demo app, because it covers how one can test command line arguments (CommandLine.arguments
), it additionally exhibits you how one can wait (dispatchMain
) for an async job, equivalent to a HTTP name by way of the community utilizing the URLSession API to complete and exit utilizing the appropriate technique when one thing fails (fatalError
) or when you attain the top of execution (exit(0)
). Only a few traces of code, but it surely comprises a lot information.
Have you ever seen the brand new shebang? When you have a number of Swift variations put in in your system, you need to use the env shebang to go along with the primary one which’s out there in your PATH.
It isn’t simply Basis, however you may import AppKit and even SwiftUI. Effectively, not below Linux in fact, since these frameworks are solely out there for macOS plus you will have Xcode put in in your system, since some stuff in Swift the toolchain remains to be tied to the IDE, however why? 😢
Anyway, again to the subject, this is the boilerplate code for a macOS software Swift script that may be began from the Terminal with one easy ./major.swift
command and nothing extra.
#!/usr/bin/env swift
import AppKit
import SwiftUI
@out there(macOS 10.15, *)
struct HelloView: View
var physique: some View
Textual content("Hiya world!")
@out there(macOS 10.15, *)
class WindowDelegate: NSObject, NSWindowDelegate
func windowWillClose(_ notification: Notification)
NSApplication.shared.terminate(0)
@out there(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: "Stop", 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, top: 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 original gist. Additionally if you’re into Storyboard-less macOS app growth, it’s best to positively check out this article by @kicsipixel. These sources helped me so much to place collectively what I wanted. I nonetheless needed to prolong 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 appeal. There is just one situation right here… the script file is getting crowded. 🙈
Swift Package deal Supervisor and macOS apps
So, if we comply with the identical logic, which means we will construct an executable bundle that may invoke AppKit associated stuff utilizing the Swift Package deal 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 supply checking, since we’ll add a platform constraint utilizing our Package deal.swift
manifest file. If you do not know a lot about how the Swift Package deal Supervisor works, please learn my SPM tutorial, or if you’re merely curious concerning the construction of a Package deal.swift file, you may learn my article concerning the Swift Package manifest file. Let’s begin with the manifest updates.
import PackageDescription
let bundle = Package deal(
identify: "MyApp",
platforms: [
.macOS(.v10_15)
],
dependencies: [
],
targets: [
.target(name: "MyApp", dependencies: []),
.testTarget(identify: "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 will 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: "Stop", 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, top: 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 every part that must be executed.
import AppKit
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.run()
The excellent news is that this strategy works, so you may develop, construct and run apps domestically, however sadly you may’t submit them to the Mac App Retailer, for the reason that ultimate software bundle will not appear like an actual macOS bundle. The binary shouldn’t be code signed, plus you may want an actual macOS goal in Xcode to submit the applying. Then why trouble with this strategy?
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. The perfect half is that SourceKit-LSP is now part of Xcode, so you do not have to put in something particular, simply configure your favourite IDE and begin coding.
You can even bundle resources, since this function is obtainable from Swift 5.3, and use them by way of the Bundle.module
variable if wanted. I already tried this, works fairly effectively, and it’s so a lot enjoyable to develop apps for the mac with out the additional overhead that Xcode comes with. 🥳