Sparkle Swift Package Manager Support: Setup, Configuration, and Common Pitfalls
Sparkle Swift Package Manager Support
Sparkle is the standard auto-update framework for macOS apps outside the App Store. Since version 2.0, it fully supports Swift Package Manager, which means you can drop the old Carthage or CocoaPods setup and manage Sparkle the same way you manage every other dependency. Here is how to set it up correctly, what actually goes wrong, and how to fix it.
Why SPM for Sparkle?
Before Sparkle 2.x, most projects pulled in the framework via Carthage, CocoaPods, or a manual xcframework embed. Each approach had its own integration tax: Carthage needed carthage update and manual framework linking, CocoaPods modified your project file, and manual embedding meant tracking releases by hand.
Swift Package Manager changed this. You add one URL in Xcode, pick a version, and the framework resolves at build time. No extra build phases, no Cartfile, no Podfile.
| Integration Method | Setup Complexity | Version Pinning | Build Phase Changes | Sandbox Compatible |
|---|---|---|---|---|
| Swift Package Manager | Low (URL + version rule) | Automatic via Package.resolved | None | Yes (with XPC) |
| Carthage | Medium (Cartfile + link phase) | Manual Cartfile.resolved | Framework embed phase | Yes (with XPC) |
| CocoaPods | Medium (Podfile + workspace) | Podfile.lock | Modifies xcodeproj | Yes (with XPC) |
| Manual xcframework | High (download + embed + sign) | Manual tracking | Framework embed phase | Yes (with XPC) |
SPM is the clear winner for new projects. If you are starting fresh or migrating an existing Carthage/CocoaPods setup, SPM is the path forward.
Adding Sparkle via SPM in Xcode
Open your project in Xcode, then:
- Go to File > Add Package Dependencies
- Enter the Sparkle repository URL:
https://github.com/sparkle-project/Sparkle - Set the version rule to Up to Next Major with a minimum of
2.0.0 - Click Add Package
- When prompted, add the
Sparklelibrary to your app target
Xcode resolves the dependency and adds it to your Package.resolved file. You should commit Package.resolved to version control so that CI builds use the exact same version.
// Package.swift (if you maintain a separate Swift package that depends on Sparkle)
dependencies: [
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0")
]
For most macOS apps, you will not have a Package.swift file at the project root. Xcode manages SPM dependencies internally via the .xcodeproj or .xcworkspace. The Package.swift route only matters if your app is itself structured as a Swift package or if you have a local package that depends on Sparkle.
Configuring the Updater in SwiftUI
Sparkle 2.x ships a SPUStandardUpdaterController that works with both AppKit and SwiftUI. The simplest integration for a SwiftUI app:
import SwiftUI
import Sparkle
@main
struct MyApp: App {
private let updaterController: SPUStandardUpdaterController
init() {
updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
Settings {
UpdaterSettingsView(updater: updaterController.updater)
}
}
}
import SwiftUI
import Sparkle
struct UpdaterSettingsView: View {
private let updater: SPUUpdater
@State private var automaticallyChecksForUpdates: Bool
@State private var automaticallyDownloadsUpdates: Bool
init(updater: SPUUpdater) {
self.updater = updater
self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates
}
var body: some View {
Form {
Toggle("Automatically check for updates",
isOn: $automaticallyChecksForUpdates)
.onChange(of: automaticallyChecksForUpdates) { newValue in
updater.automaticallyChecksForUpdates = newValue
}
Toggle("Automatically download updates",
isOn: $automaticallyDownloadsUpdates)
.onChange(of: automaticallyDownloadsUpdates) { newValue in
updater.automaticallyDownloadsUpdates = newValue
}
}
.padding()
.frame(width: 350)
}
}
You also need a "Check for Updates" menu item. Add it to your MenuBarExtra or Commands:
struct CheckForUpdatesView: View {
@ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel
private let updater: SPUUpdater
init(updater: SPUUpdater) {
self.updater = updater
self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater)
}
var body: some View {
Button("Check for Updates...", action: updater.checkForUpdates)
.disabled(!checkForUpdatesViewModel.canCheckForUpdates)
}
}
final class CheckForUpdatesViewModel: ObservableObject {
@Published var canCheckForUpdates = false
private var cancellable: AnyCancellable?
init(updater: SPUUpdater) {
cancellable = updater.publisher(for: \.canCheckForUpdates)
.assign(to: \.canCheckForUpdates, on: self)
}
}
Note
You need to import Combine in the file containing CheckForUpdatesViewModel. The publisher(for:) API returns a Combine publisher.
Architecture: How Sparkle SPM Delivery Works
The SPM package delivers the same framework binary as other distribution methods. The update flow itself does not change based on how you integrated Sparkle. What changes is how Xcode resolves and links the framework at build time.
Info.plist Keys You Must Set
Sparkle reads its configuration from your app's Info.plist. These keys are required:
<key>SUFeedURL</key>
<string>https://yourserver.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>your-base64-encoded-ed25519-public-key</string>
Generate the EdDSA key pair using the generate_keys tool that ships with Sparkle:
# After adding Sparkle via SPM, the tool is at:
./DerivedData/YourApp/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_keys
Run it once. It stores the private key in your Keychain and prints the public key. Put the public key in SUPublicEDKey.
Warning
Never commit the private EdDSA key to version control. It lives in your macOS Keychain. If you lose it, you cannot sign updates and existing users will not be able to update. Back up the Keychain item.
Other useful (but optional) keys:
| Key | Default | Purpose |
|---|---|---|
| SUEnableAutomaticChecks | NO | Auto-check on launch |
| SUScheduledCheckInterval | 86400 (1 day) | Seconds between checks |
| SUAllowsAutomaticUpdates | YES | Download + install without asking |
| SUEnableSystemProfiling | NO | Send anonymous system info to appcast |
Sandboxed Apps and the XPC Service
If your app uses the App Sandbox entitlement (and it should, for notarization and distribution outside the Mac App Store), Sparkle cannot write to the app bundle directly. It needs an XPC service to perform the privileged install step.
Sparkle's SPM package includes the Sparkle library target, but the XPC services (Installer.xpc and Downloader.xpc) are delivered as pre-built bundles inside the package artifacts. You need to embed them manually:
- In Xcode, select your app target
- Go to Build Phases > Copy Bundle Resources (or Embed XPC Services if available)
- Add the XPC service bundles from the Sparkle package artifacts
The exact path depends on your DerivedData location, but it is typically:
DerivedData/YourApp/SourcePackages/artifacts/sparkle/Sparkle/XPCServices/
├── org.sparkle-project.InstallerConnection.xpc
├── org.sparkle-project.InstallerLauncher.xpc
└── org.sparkle-project.InstallerStatus.xpc
Warning
If you skip embedding the XPC services, Sparkle will silently fail to install updates in sandboxed apps. The download succeeds, the signature verifies, but the install step does nothing. Check Console.app for XPC connection errors if updates seem to download but never apply.
Creating the Appcast
The appcast is an XML file that lists your app versions, download URLs, and EdDSA signatures. Sparkle ships a generate_appcast tool:
# Sign your .zip or .dmg and generate the appcast
./DerivedData/YourApp/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_appcast \
/path/to/your/release-archives/
This scans the directory for .zip or .dmg files, reads the CFBundleShortVersionString from each app bundle, signs each archive with your EdDSA private key, and writes (or updates) an appcast.xml file.
A minimal appcast entry looks like:
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>MyApp Changelog</title>
<item>
<title>Version 1.2.0</title>
<sparkle:version>42</sparkle:version>
<sparkle:shortVersionString>1.2.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>13.0</sparkle:minimumSystemVersion>
<pubDate>Sun, 06 Apr 2026 12:00:00 +0000</pubDate>
<enclosure
url="https://yourserver.com/releases/MyApp-1.2.0.zip"
length="8472301"
type="application/octet-stream"
sparkle:edSignature="BASE64_SIGNATURE_HERE"
/>
</item>
</channel>
</rss>
Host this file on any HTTPS server. GitHub Releases, S3, Cloudflare R2, or your own domain all work fine.
Common Pitfalls
ls YourApp.app/Contents/XPCServices/.SUPublicEDKey after regenerating keys. If you run generate_keys more than once, you get a new key pair. Existing users have the old public key baked in. Their app will reject updates signed with the new key. Only generate keys once per product.SUFeedURL.CFBundleVersion. Sparkle compares sparkle:version (which maps to CFBundleVersion, the build number) to decide if an update is newer. If you forget to bump it, users will never see the update even if CFBundleShortVersionString changed.Minimal Working Example
Here is a complete, minimal SwiftUI app with Sparkle auto-updates via SPM:
// MyApp.swift
import SwiftUI
import Sparkle
@main
struct MyApp: App {
private let updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
var body: some Scene {
WindowGroup {
Text("Hello, World!")
.frame(width: 300, height: 200)
}
.commands {
CommandGroup(after: .appInfo) {
Button("Check for Updates...") {
updaterController.checkForUpdates(nil)
}
}
}
}
}
And in your Info.plist:
<key>SUFeedURL</key>
<string>https://example.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>YOUR_ED25519_PUBLIC_KEY</string>
That is the entire integration. Build, archive, sign with generate_appcast, upload, and your users get automatic updates.
CI/CD Integration
If you use Xcode Cloud or GitHub Actions, SPM dependencies resolve automatically during xcodebuild. The only extra step is running generate_appcast after archiving:
# In your CI script, after xcodebuild archive + export
SPARKLE_BIN="$(find DerivedData -name generate_appcast -type f | head -1)"
"$SPARKLE_BIN" ./release-artifacts/
# Upload appcast.xml and the .zip to your hosting
aws s3 cp ./release-artifacts/appcast.xml s3://your-bucket/appcast.xml
aws s3 cp ./release-artifacts/MyApp-*.zip s3://your-bucket/releases/
The generate_appcast tool reads the EdDSA private key from the Keychain. On CI, you will need to import the key into the build machine's Keychain first. Sparkle provides a generate_keys -p flag to export the private key for this purpose.
Wrapping Up
Sparkle via Swift Package Manager is the simplest way to ship auto-updates for macOS apps distributed outside the App Store. Add the package URL, configure two Info.plist keys, embed the XPC services for sandboxed builds, and generate your appcast. The most common failures come from missing XPC services and mismatched EdDSA keys, both of which are avoidable with a single checklist run before your first release.
Fazm is an open source macOS AI agent. Open source on GitHub.