Integrating Sparkle in SwftUI Mac Apps
If you're not distributing a Mac app via the Mac App Store, you need to rely on an alternate ways to push updates out to your users. The best-known option is the Sparkle project — chances are, if you got a Mac app direct from a software publisher, it's using Sparkle to let you know there's a new version available, and to download and install the update.
Now, if you're relatively new to developing Mac apps, you'll quickly learn that finding examples or sample code for doing a thing in AppKit is nowhere near as common as it is for UIKit. That's equally true for working in SwiftUI on a Mac app, as opposed to an iOS app. Maybe worse; there seem to be more sharp edges on the Mac side when building with SwiftUI; maybe Mac developers are mostly sticking to AppKit until things stabilize.
So to contribute, I thought I'd provide a little writeup of how I integrated Sparkle into WriteFreely for Mac, which is a SwiftUI-lifecycle app. This isn't meant to be a step-by-step deep dive on the process; rather, this should get you set up with a basic implementation that serves updates on both a release and beta channel. We're also using the stable 1.x branch, rather than the 2.x beta, which means you'll need to tweak things a bit if you need your app to be sandboxed.
Fork The Project
Recently, on the (stable) 1.x branch, the maintainers of Sparkle added Swift Package Manager support. The WriteFreely SwiftUI project already uses SPM to pull in the Swift API wrapper, which is great!
As of v1.24.0 (code name “Big Oof”) of Sparkle, integration via SPM is available. Normally, you'd be able to define which version(s) are used in your project by choosing a tag and, for example, only allowing automatic updates to the next minor version, for example. This isn't yet supported for Sparkle, so we have to use the master
branch or a specifc commit.
Since we don't want Sparkle updating on us every time someone pushes a new version to master
, we forked the project repository and use that in our Xcode project. Always thoroughly vet your dependencies in staging, before upgrading them in prod!
You'll also want to download the Sparkle-for-Swift-Package-Manager.zip
archive. You'll need this to run the tools that generate signing keys for your app, and for generating the appcasts for new updates.
Setting Up The App
First, you'll want to generate the signing keys, per step 3 of the project documentation's “Basic Setup” guide.
Quick Segue: I ran into some signing issues trying to run the tools in the archive's /bin
directory — specifically, an alert saying something like:
“generate_keys” can't be opened because Apple cannot check it for malicious software.
This comment in a GitHub issue I opened offers a workaround.
Make a note of the public signing key, so that you can add it to the SUPublicEDKey
property in the Mac target's info.plist file. While you have that plist file open in your Xcode project, go ahead and add the URL from which you'll be serving appcasts and update binaries to the SUFeedURL
property. You can check out WriteFreely's info.plist file for reference.
Onto The Code
There are two main things you'll need to add to the app (beyond the changes to the plist file): a way to initialize the updater object when the app launches, and some UI to set various options.
Setup is done in the Mac app's applicationWillFinishLaunching()
App Delegate method. Except... this is a SwiftUI-lifecycle app, so there is no App Delegate. But you can add one! Have a look at WriteFreely's App Delegate: it checks UserDefaults to see if update settings exist, then sets the automaticallyChecksForUpdates
and feedURL
appropriately based on what it finds. Finally, if the option to automatically check for updates is enabled, it calls checkForUpdatesInBackground()
once the application has finished launching. Call it from the main App
struct with NSApplicationDelegateAdaptor:
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
While you're in there, you may as well add a menu item to check for updates. Drop the following into a .commands
modifier on your WindowGroup:
#if os(macOS)
CommandGroup(after: .appInfo, addition: {
Button("Check For Updates") {
SUUpdater.shared()?.checkForUpdates(self)
}
})
#endif
With that in place, you now just need a way to let your users set some preferences. For WriteFreely, we added an “Updates” tab to the Preferences window:
It's a fairly straightforward SwiftUI View that sets those UserDefaults settings (via the @AppStorage
property wrapper) that are checked by the App Delegate.
There Is No Step Three
With those three things in place —the updates to info.plist, the App Delegate, and the preferences UI— you're done! Sparkle is set up for your Mac app. You can create an archive of your Mac app, get it notarized by Apple with your Developer ID, and export the .app archive to disk just as you did before; all that's left is publishing an update, which is covered in the Sparkle publishing guide — essentially, that means running generate_appcast
in the directory with all of your app updates, and uploading the files to the location you specified as your SUFeedURL
.
We've documented the process for updating the software in a tech note in the repo.
I hope this has been helpful! If you've got any questions or comments, send a tweet to @AngeloStavrow, a toot to @AngeloStavrow@mastodon.technology, or a reply on Micro.blog.
Enter your email to subscribe to updates:
You can also subscribe via RSS or follow @angelo@write.as
on Mastodon.