The best way to construct macOS apps utilizing solely the Swift Package deal Supervisor?


Swift scripts and macOS apps

Swift compiler 101, you may create, construct and run a Swift file utilizing the swiftc command. Think about the most straightforward Swift program that we are able to all think about in a major.swift file:

print("Hey world!")

In Swift if we wish to print one thing, we do not even must import the Basis framework, we are able to 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 are able to take this one step additional by auto-invoking the Swift compiler underneath the hood with a shebang).

#! /usr/bin/swift

print("Hey world!")

Now when you merely run the ./major.swift file it will print out the well-known “Hey world!” textual content. 👋

Due to the program-loader mechanism and naturally the Swift interpreter we are able to skip an additional step and run our single-source Swift code as straightforward as a daily shell script. The excellent news is that we are able to import all form of system frameworks which are a part of the Swift toolchain. With the assistance of Basis we are able to construct fairly helpful or fully ineffective command line utilities.

#!/usr/bin/env swift

import Basis
import Dispatch

guard CommandLine.arguments.rely == 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) { knowledge, 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 knowledge = knowledge else {
        fatalError("Error: lacking response knowledge")
    }

    do {
        let decoder = JSONDecoder()
        let todos = attempt decoder.decode([Todo].self, from: knowledge)
        print("Checklist of todos:")
        print(todos.map { " - [" + ($0.completed ? "✅" : "❌") + "] ($0.title)" }.joined(separator: "n"))
        exit(0)
    }
    catch {
        fatalError("Error: (error.localizedDescription)")
    }
}
job.resume()
dispatchMain()

In the event you name this instance with a URL that may return an inventory of todos it will 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 methods to examine command line arguments (CommandLine.arguments), it additionally reveals you methods to wait (dispatchMain) for an async job, akin 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)). Only a few strains of code, however it accommodates a lot data.

Have you ever observed the brand new shebang? When you have a number of Swift variations put in in your system, you should use the env shebang to go together with the primary one which’s accessible in your PATH.

It isn’t simply Basis, however you may import AppKit and even SwiftUI. Nicely, not underneath Linux in fact, since these frameworks are solely accessible for macOS plus you have to 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, 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

@accessible(macOS 10.15, *)
struct HelloView: View {
    var physique: some View {
        Textual content("Hey 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: "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, 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.heart()
        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 undoubtedly check out this text 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 allure. 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 are able to 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 are able to separate the parts into standalone information, we are able to additionally take away the provision 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 in regards to the construction of a Package deal.swift file, you may learn my article in regards to the Swift Package deal 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 are able to place the HelloView struct into a brand new HelloView.swift file.

import SwiftUI

struct HelloView: View {
    var physique: some View {
        Textual content("Hey 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, 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.heart()
        window.makeKeyAndOrderFront(window)
        
        NSApp.setActivationPolicy(.common)
        NSApp.activate(ignoringOtherApps: true)
    }
}

Lastly we are able to replace the primary.swift file and provoke every thing that must be finished.

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 regionally, however sadly you may’t submit them to the Mac App Retailer, because the closing software bundle will not appear like an actual macOS bundle. The binary isn’t code signed, plus you will want an actual macOS goal in Xcode to submit the appliance. Then why trouble with this strategy?

Nicely, 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 most effective 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.

It’s also possible to bundle sources, since this characteristic is on the market from Swift 5.3, and use them via 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. 🥳



Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles