This is a recording of my own odyssey developing my first Swift command-line (CLI) app to change the default browser on macOS. For this tutorial I’ll ignore the fact that there is already a brew package that does this. So, how difficult can it be? Turns out, not that difficult, actually. The difficult part is to gather all information to build the complete »flow«.
The use case: change the default browser in macOS.
Just a small note for the audience: I expect that you know how to do stuff on the command-line and you know at least one other programming language. If possible I drop links to additional material.
So here I am, no clue about software development for macOS. My first idea was to fire up Xcode and create a new project. This worked pretty well, I could write my code and run/debug it.
However, I would now, after I’ve done everything, recommend a slightly different
approach: Swift comes with a CLI tool, conveniently called
swift to create new
packages (related blog post).
Surprise, surprise: I figured out that I have to create an app (and not a library). I
called my app »defbro« (default browser), because »defaultbrowser« was already in
mkdir defbro swift package init --type executable
The big plus is that newer Xcode versions can just open Swift package repositories without running any additional commands. This gives us the boilerplate to create our default browser switcher. Let’s try it:
swift build swift run
Done. That seems to work. Now you can fire up Xcode and open the directory.
Creating a project with Xcode is straight forward, but the added benefit of Swift packages is easier to handle via the package description. For most CLI scripts, parsing command arguments is required. apple/swift-argument-parser seems nice, it is even supported by Apple.
To use the argument parser, you have to adapt the
Package.swift, resulting in
something like this:
let package = Package( name: "defbro", dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "0.4.0"), ], targets: [ .target( name: "defbro", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), ]), .testTarget( name: "defbroTests", dependencies: ["defbro"]), ] )
With the basis set up, we can now dive into Swift basics. Haha, joke, I’m not diving into Swift here, there are plenty resources you can tap – let’s introduce some homebrew lingo already – into. Two references that helped me were
I started off with the code from existing repositories, but found out that I do not want to write my code in some C-based language and that I do want a clear build, without deprecation warnings. In particular, I saw
'LSCopyDefaultHandlerForURLScheme' was deprecated in macOS 10.15: Use LSCopyDefaultApplicationURLForURL() instead. 'LSCopyAllHandlersForURLScheme' was deprecated in macOS 10.15: Use LSCopyApplicationURLsForURL() instead.
which led me to a rewrite. After a while, I successfully hacked the logic together, you can see the end result on github.
Homebrew is a totally different beast, but to wrap your head around it, you only need to know:
Formulae and Taps you can create with the
A straight forward approach is to add a version tag with
git tag v0.0.1 to the source
git push --tags it to github and create a formula form the automatically
generated tag tarball, an example you can find on
the defbro repo.
.tar.gz link and run
brew create https://.../tags/v0.0.1.tar.gz with
it. I ended up with
class Defbro < Formula desc "Set the default browser from command-line" homepage "https://github.com/jwbargsten/defbro" url "https://github.com/jwbargsten/defbro/archive/refs/tags/v0.0.1.tar.gz" sha256 "90c5bfa02037cdcdb5356b935298d52d854f3ab1368bc4663b77e99e3287f8a6" license "MIT" depends_on xcode: [">= 11.2", :build] depends_on macos: [ :catalina, :big_sur, ] def install system "swift", "build", "--disable-sandbox", "--configuration", "release" bin.install ".build/release/defbro" end test do system "true" end end
In case of doubt, you can always clone homebrew-core and grep through the existing core formulae.