HomeiOS DevelopmentTips on how to construct macOS apps utilizing solely the Swift Bundle...

Tips on how to construct macOS apps utilizing solely the Swift Bundle Supervisor?


Swift scripts and macOS apps

Swift compiler 101, you’ll be able to 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 essential.swift file:

print("Hi there 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 essential.swift 	# compile essential.swift
chmod +x essential 		# add the executable permission
./essential 			# run the binary

The excellent news that we will take this one step additional by auto-invoking the Swift compiler beneath the hood with a shebang.

#! /usr/bin/swift

print("Hi there world!")

Now if you happen to merely run the ./essential.swift file it’s going to print out the well-known “Hi there 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 straightforward as a daily shell script. The excellent news is that we will import all kind of system frameworks which can be a part of the Swift toolchain. With the assistance of Basis we will 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 process = 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 = strive decoder.decode([Todo].self, from: knowledge)
        print("Listing of todos:")
        print(todos.map { " - [" + ($0.completed ? "✅" : "❌") + "] ($0.title)" }.joined(separator: "n"))
        exit(0)
    }
    catch {
        fatalError("Error: (error.localizedDescription)")
    }
}
process.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.

./essential.swift https://jsonplaceholder.typicode.com/todos

Sure, you’ll be able to say that this script is totally ineffective, however for my part it is a tremendous demo app, because it covers how one can examine command line arguments (CommandLine.arguments), it additionally exhibits you how one can wait (dispatchMain) for an async process, equivalent to a HTTP name via the community utilizing the URLSession API to complete and exit utilizing the fitting technique when one thing fails (fatalError) or if you happen to attain the tip of execution (exit(0)). Only a few strains of code, nevertheless it incorporates a lot information.

Have you ever seen the brand new shebang? In case 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 out there in your PATH.

It is not simply Basis, however you’ll be able to import AppKit and even SwiftUI. Nicely, not beneath Linux in fact, since these frameworks are solely out there for macOS plus you’ll need 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, here is the boilerplate code for a macOS software Swift script that may be began from the Terminal with one easy ./essential.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("Hi there 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 dimension = CGSize(width: 480, top: 270)
        window.setContentSize(dimension)
        window.styleMask = [.closable, .miniaturizable, .resizable, .titled]
        window.delegate = windowDelegate
        window.title = "My Swift Script"

        let view = NSHostingView(rootView: HelloView())
        view.body = CGRect(origin: .zero, dimension: dimension)
        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 in case you are into Storyboard-less macOS app growth, it’s best to undoubtedly check out this text by @kicsipixel. These sources helped me loads 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, which means we will construct an executable package deal that may invoke AppKit associated stuff utilizing the Swift Bundle Supervisor. Simple as a pie. 🥧

mkdir MyApp
cd MyApp 
swift package deal init --type=executable

Now we will separate the elements into standalone information, we will additionally take away the supply 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 in case you are merely curious concerning the construction of a Bundle.swift file, you’ll be able to learn my article concerning the Swift Bundle manifest file. Let’s begin with the manifest updates.


import PackageDescription

let package deal = 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("Hi there 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: "Stop", motion: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
        let mainMenu = NSMenu(title: "My Swift Script")
        mainMenu.addItem(appMenu)
        NSApplication.shared.mainMenu = mainMenu
        
        let dimension = CGSize(width: 480, top: 270)
        window.setContentSize(dimension)
        window.styleMask = [.closable, .miniaturizable, .resizable, .titled]
        window.delegate = windowDelegate
        window.title = "My Swift Script"

        let view = NSHostingView(rootView: HelloView())
        view.body = CGRect(origin: .zero, dimension: dimension)
        view.autoresizingMask = [.height, .width]
        window.contentView!.addSubview(view)
        window.heart()
        window.makeKeyAndOrderFront(window)
        
        NSApp.setActivationPolicy(.common)
        NSApp.activate(ignoringOtherApps: true)
    }
}

Lastly we will replace the primary.swift file and provoke all the pieces 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’ll be able to develop, construct and run apps regionally, however sadly you’ll be able to’t submit them to the Mac App Retailer, for the reason that remaining software package deal will not appear like an actual macOS bundle. The binary is just not code signed, plus you may want an actual macOS goal in Xcode to submit the appliance. Then why trouble with this method?

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. 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 may as well bundle sources, since this characteristic 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. 🥳



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments