A Swift package for integrating automatic app updates into macOS applications via the PixelPantry distribution platform.
- macOS 13.0+
- Swift 5.9+
- Non-sandboxed app (required for automatic installation)
Important: Your app must have
com.apple.security.app-sandboxset tofalsein your entitlements file andENABLE_APP_SANDBOX = NOin your Xcode build settings for automatic updates to work. Sandboxed apps cannot replace themselves or request admin privileges.
Add PixelPantry to your project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/pixel-pantry/PixelPantry.git", from: "1.0.9")
]Or in Xcode: File > Add Package Dependencies > Enter the repository URL:
https://github.com/pixel-pantry/PixelPantry.git
- Register your app at PixelPantry Developer Portal
- Get your App Key (starts with
pk_) and App Secret (starts withsk_)
Configure the SDK early in your app's lifecycle (e.g., in your AppDelegate):
import PixelPantry
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
PixelPantry.configure(
bundleId: "com.yourcompany.yourapp",
appKey: "pk_your_app_key",
appSecret: "sk_your_app_secret"
)
}
}The easiest way to add update checking:
func applicationDidFinishLaunching(_ notification: Notification) {
PixelPantry.configure(
bundleId: "com.yourcompany.yourapp",
appKey: "pk_your_app_key",
appSecret: "sk_your_app_secret"
)
// Check for updates after a short delay
Task {
try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds
await PixelPantry.checkForUpdatesOnLaunch()
}
}This will:
- Silently check for updates
- Show a native alert if an update is available
- If the user clicks "Install Now", show the update window with progress
- Download, install, and relaunch the app automatically
// Shows alert first, then update window if user accepts
await PixelPantry.checkForUpdatesOnLaunch(showAlertFirst: true)// Shows update window directly (no confirmation alert)
await PixelPantry.checkForUpdatesOnLaunch(showAlertFirst: false)
// Or manually for a specific update
await PixelPantry.showUpdateWindowIfAvailable()// Returns true if update was found and user clicked "Install Now"
let userAccepted = await PixelPantry.checkAndPromptForUpdate()For complete control over the update process:
// 1. Check for updates
let result = await PixelPantry.checkForUpdates()
switch result {
case .available(let update):
print("Update available: \(update.version)")
print("Release notes: \(update.releaseNotes)")
// 2. Download with progress tracking
let downloadResult = await PixelPantry.downloadUpdate(update) { progress in
print("Download progress: \(Int(progress * 100))%")
}
switch downloadResult {
case .success(let fileURL):
// 3. Install and relaunch
let installResult = await PixelPantry.installUpdate(from: fileURL)
if case .failure(let error) = installResult {
print("Installation failed: \(error)")
}
case .failure(let error):
print("Download failed: \(error)")
}
case .upToDate:
print("Already on latest version")
case .error(let error):
print("Error checking for updates: \(error)")
}if case .available(let update) = await PixelPantry.checkForUpdates() {
let result = await PixelPantry.downloadAndInstall(update) { progress in
print("Download: \(Int(progress * 100))%")
}
switch result {
case .success:
print("Update installed, app will relaunch")
case .failure(let error):
print("Update failed: \(error)")
}
}import PixelPantry
import SwiftUI
struct SettingsView: View {
@State private var showingUpdate = false
var body: some View {
VStack {
Button("Check for Updates") {
showingUpdate = true
}
}
.sheet(isPresented: $showingUpdate) {
PixelPantryUpdateView()
}
}
}// Show window if update is available
await PixelPantry.showUpdateWindowIfAvailable()
// Or show for a specific update
if case .available(let update) = await PixelPantry.checkForUpdates() {
await MainActor.run {
PixelPantry.showUpdateWindow(for: update)
}
}For automatic updates to work, you must disable the App Sandbox:
In your .entitlements file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>In Xcode, go to your target's Build Settings and set:
ENABLE_APP_SANDBOX=NO
Note: If you need to distribute on the Mac App Store (which requires sandboxing), you cannot use automatic updates. The SDK will fall back to copying the update to the user's Desktop/Downloads folder with manual installation instructions.
- Direct Install - First tries to copy the new app directly (works if you have write permission to the install location)
- Admin Privileges - If direct install fails, shows macOS password dialog to request admin privileges
- Manual Fallback - If both fail (e.g., sandboxed app), copies to Desktop and shows instructions
let result = await PixelPantry.checkForUpdates()
if case .error(let error) = result {
switch error {
case .notConfigured:
print("Call PixelPantry.configure() first")
case .networkError(let message):
print("Network error: \(message)")
case .invalidResponse(let statusCode, let message):
print("Server error \(statusCode ?? 0): \(message)")
case .downloadFailed(let reason):
print("Download failed: \(reason)")
case .verificationFailed:
print("File hash verification failed")
case .installationFailed(let reason):
print("Installation failed: \(reason)")
}
}The installer automatically handles:
.zip- ZIP archives (extracted usingditto).dmg- Disk images (mounted, app extracted, unmounted)
- All API requests are signed using HMAC-SHA256 with your app secret
- Downloaded files are verified against SHA256 checksums (when provided by server)
- Existing apps are moved to Trash before replacement (recoverable)
- Admin password is requested via macOS Security framework (never stored)
// Get current app version
let currentVersion = PixelPantry.currentVersion // e.g., "1.0.0"
// Get current macOS version
let macOSVersion = PixelPantry.currentMacOSVersion // e.g., "14.0"
// Check if SDK is configured
let isReady = PixelPantry.isConfigured // true/falseimport SwiftUI
import PixelPantry
@main
struct MyApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
// Configure PixelPantry
PixelPantry.configure(
bundleId: "com.example.myapp",
appKey: "pk_abc123",
appSecret: "sk_xyz789"
)
// Check for updates on launch
Task {
try? await Task.sleep(nanoseconds: 3_000_000_000)
await PixelPantry.checkForUpdatesOnLaunch()
}
}
}The user clicked Cancel on the password dialog.
- Ensure your app is not sandboxed (check both entitlements and build settings)
- Clean build folder (Product > Clean Build Folder) and rebuild
- Make sure
ENABLE_APP_SANDBOX = NOin build settings - Rebuild the app after changing entitlements
Check the Xcode console for [PixelPantry] log messages to see where it's failing.
MIT License - see LICENSE file for details.