
Macos Auto Update
- 261 installs
- 641 repo stars
- Updated May 27, 2026
- fayazara/macos-app-skills
Add reliable auto-update to a macOS desktop app using Sparkle or native update flows, signing, delta delivery, and user-safe restart behavior.
About
Teaches macOS app auto-update implementation: integrating Sparkle or equivalent update services, configuring signed feeds, handling notarization, and delivering safe in-app updates with restart and rollback considerations for desktop releases.
- Sparkle feed and signature setup
- Code signing and notarization steps
- Delta update configuration
- User consent and restart UX
- Version channel management
Macos Auto Update by the numbers
- 261 all-time installs (skills.sh)
- Ranked #66 of 248 Release Management skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fayazara/macos-app-skills --skill macos-auto-updateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 261 |
|---|---|
| repo stars | ★ 641 |
| Last updated | May 27, 2026 |
| Repository | fayazara/macos-app-skills ↗ |
What it does
Add reliable auto-update to a macOS desktop app using Sparkle or native update flows, signing, delta delivery, and user-safe restart behavior.
Files
Sparkle Auto-Update for macOS Apps
This skill adds Sparkle auto-update support to a native macOS app. Sparkle is the standard open-source framework for macOS app updates outside the Mac App Store.
Overview
The implementation has 4 parts:
1. SPM dependency -- add the Sparkle package to the Xcode project 2. UpdaterManager.swift -- a singleton that wraps SPUStandardUpdaterController 3. Info.plist keys -- SUFeedURL and SUPublicEDKey 4. UI integration -- "Check for Updates" button in settings/menu bar
Step 1: Add Sparkle via SPM
In Xcode: File > Add Package Dependencies > enter:
https://github.com/sparkle-project/SparkleUse the "Up to Next Major Version" rule with 2.0.0. Add the Sparkle framework to your app target.
Or add it to Package.swift if your project uses one:
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0")Step 2: Create UpdaterManager.swift
Copy references/UpdaterManager.swift into your project. This is a singleton that:
- Creates
SPUStandardUpdaterControllerearly (beforeapplicationDidFinishLaunchingreturns) - Publishes
canCheckForUpdatesfor UI binding - Exposes
automaticallyChecksForUpdatestoggle - Skips all update logic in DEBUG builds (so you don't get update prompts during development)
- For menu-bar-only apps: temporarily switches to
.regularactivation policy before showing the update window
The key design decisions in this file:
- `startingUpdater: false` in the initializer, then calling
start()explicitly inapplicationDidFinishLaunching. This gives you control over timing. - DEBUG guards on
start()andcheckForUpdates(). Sparkle should never run in debug builds -- it would try to update your debug app with a release build. - `ObservableObject` with `@Published` (not
@Observable) because we need the Combinepublisher(for:)bridge from Sparkle's KVO.
Step 3: Configure Info.plist
Add these keys to your app's Info.plist:
<key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/OWNER/REPO/main/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>YOUR_PUBLIC_EDDSA_KEY_HERE</string>
<key>SUEnableInstallerLauncherService</key>
<true/>Generating EdDSA Keys
Sparkle uses EdDSA (Ed25519) signing. Generate a keypair:
# Find generate_keys in your DerivedData after building the project with Sparkle
find ~/Library/Developer/Xcode/DerivedData -name "generate_keys" -type f 2>/dev/null | head -1Run it:
/path/to/generate_keysThis prints the public key and stores the private key in your Keychain. Put the public key in SUPublicEDKey in Info.plist. The private key stays in Keychain and is used by sign_update during release.
Step 4: Wire Into App
App Delegate
final class AppDelegate: NSObject, NSApplicationDelegate {
private let updaterManager = UpdaterManager.shared
func applicationDidFinishLaunching(_ notification: Notification) {
updaterManager.start()
}
}The UpdaterManager.shared property must be accessed early so the SPUStandardUpdaterController is created before the app finishes launching. Referencing it in the AppDelegate property ensures this.
Settings UI (About Pane)
struct AboutSettingsPane: View {
@ObservedObject private var updaterManager = UpdaterManager.shared
var body: some View {
Form {
Section("Updates") {
Toggle(isOn: Binding(
get: { updaterManager.automaticallyChecksForUpdates },
set: { updaterManager.automaticallyChecksForUpdates = $0 }
)) {
Text("Automatically check for updates")
}
Button("Check for Updates...") {
updaterManager.checkForUpdates()
}
.disabled(!updaterManager.canCheckForUpdates)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
}
}Menu Bar (optional)
Button {
updaterManager.checkForUpdates()
} label: {
Label("Check for Updates...", systemImage: "arrow.down.circle")
}
.disabled(!updaterManager.canCheckForUpdates)Step 5: Create Initial Appcast
Create an appcast.xml at the root of your repo. It starts empty and gets populated by your release process:
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>YourApp</title>
<description>Most recent changes for YourApp.</description>
<language>en</language>
</channel>
</rss>Host this file on GitHub (raw URL) or any static file host. The URL must match SUFeedURL in Info.plist.
Menu-Bar-Only Apps
If your app runs as .accessory (no Dock icon), Sparkle's update window won't appear unless you temporarily switch to .regular. The reference UpdaterManager handles this in checkForUpdates():
func checkForUpdates() {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
controller.checkForUpdates(nil)
}The app reverts to .accessory when the update window closes (handled by your existing activation policy manager).
Hardened Runtime Entitlements
If your app uses Hardened Runtime (required for notarization), no special Sparkle entitlements are needed. Sparkle 2.x works with the standard hardened runtime configuration.
Appcast Item Format
Each release in the appcast looks like this (for reference when building release tooling):
<item>
<title>Version 1.2 (Build 5)</title>
<pubDate>Mon, 26 May 2026 12:00:00 +0000</pubDate>
<sparkle:version>5</sparkle:version>
<sparkle:shortVersionString>1.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<description><![CDATA[<ul><li>New feature</li><li>Bug fix</li></ul>]]></description>
<enclosure url="https://github.com/OWNER/REPO/releases/download/v1.2/YourApp.dmg"
type="application/octet-stream"
sparkle:edSignature="BASE64_EDDSA_SIGNATURE"
length="FILE_SIZE_BYTES" />
</item>sparkle:version=CFBundleVersion(build number)sparkle:shortVersionString=CFBundleShortVersionString(marketing version)sparkle:edSignature= output ofsign_update YourApp.dmglength= file size in bytes
//
// UpdaterManager.swift
// {{AppName}}
//
// Manages Sparkle auto-update lifecycle.
// Uses ObservableObject (not @Observable) because we need the Combine
// publisher bridge from Sparkle's KVO-based canCheckForUpdates.
//
import AppKit
import Combine
import Foundation
import Sparkle
@MainActor
final class UpdaterManager: NSObject, ObservableObject {
static let shared = UpdaterManager()
private let controller: SPUStandardUpdaterController
@Published var canCheckForUpdates = false
var automaticallyChecksForUpdates: Bool {
get { controller.updater.automaticallyChecksForUpdates }
set { controller.updater.automaticallyChecksForUpdates = newValue }
}
private override init() {
// startingUpdater: false -- we call start() explicitly in applicationDidFinishLaunching.
controller = SPUStandardUpdaterController(
startingUpdater: false,
updaterDelegate: nil,
userDriverDelegate: nil
)
super.init()
controller.updater.publisher(for: \.canCheckForUpdates)
.assign(to: &$canCheckForUpdates)
}
/// Call from applicationDidFinishLaunching to begin the automatic update schedule.
func start() {
#if DEBUG
// Never check for updates in debug builds.
return
#else
controller.startUpdater()
#endif
}
/// Manually trigger an update check. For menu-bar-only apps, this switches
/// to .regular activation policy so Sparkle's update window can appear.
func checkForUpdates() {
#if DEBUG
return
#else
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
controller.checkForUpdates(nil)
#endif
}
}