Sparkle Swift Package Manager Support: Setup, Configuration, and Common Pitfalls

Matthew Diakonov··12 min read

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:

  1. Go to File > Add Package Dependencies
  2. Enter the Sparkle repository URL: https://github.com/sparkle-project/Sparkle
  3. Set the version rule to Up to Next Major with a minimum of 2.0.0
  4. Click Add Package
  5. When prompted, add the Sparkle library 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

Your App(SPUUpdater)fetchesAppcast XML(SUFeedURL)parsesVersion Check(semver compare)newer?Download .zip(EdDSA signed)Verify + Install(XPC service)Relaunch(updated app)Sparkle Update Flow (SPM-delivered framework)

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:

  1. In Xcode, select your app target
  2. Go to Build Phases > Copy Bundle Resources (or Embed XPC Services if available)
  3. 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

Missing XPC services in sandboxed builds. The most common failure. Sparkle downloads the update, verifies the signature, then silently fails because the XPC service is not embedded. Always check the app bundle contents: ls YourApp.app/Contents/XPCServices/.
Wrong 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.
SPM version conflicts with other packages. If another dependency also pulls in Sparkle (rare but possible), SPM will error with a version conflict. Pin to the same version range in both places or let the higher-level package resolve.
Appcast URL is HTTP, not HTTPS. Sparkle 2.x rejects insecure feed URLs by default. App Transport Security also blocks plain HTTP. Always use HTTPS for your SUFeedURL.
Forgetting to increment 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.

Related Posts