Overview
UpdateService fetches the latest release metadata from the GitHub API and determines if a newer version is available. It works in conjunction with AppUpdater which handles the actual DMG download and installation.
Located at: Fiaxe/Services/UpdateService.swift:32
Two-Part Update System
- UpdateService (this page) - Fetches release metadata from GitHub API
- AppUpdater - Downloads DMG and installs updates
Together they provide automatic updates by checking GitHub releases and installing new versions.
UpdateService
Type Definition
Implemented as an enum with static methods (no instances).
checkForUpdate()
Fetches the latest release from GitHub and returns it if newer than the running version.
static func checkForUpdate() async throws -> GitHubRelease?
Returns: GitHubRelease if a newer version is available, nil if current version is up to date.
Throws: Network errors or JSON decoding errors.
Implementation Details
// Example from UpdateService.swift:36-50
static func checkForUpdate() async throws -> GitHubRelease? {
var request = URLRequest(url: apiURL, cachePolicy: .reloadIgnoringLocalCacheData)
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
let (data, _) = try await URLSession.shared.data(for: request)
let release = try JSONDecoder().decode(GitHubRelease.self, from: data)
let latestVersion = release.tagName.trimmingCharacters(in: .init(charactersIn: "v"))
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0"
if latestVersion.compare(currentVersion, options: .numeric) == .orderedDescending {
return release
}
return nil
}
The method:
- Fetches the latest release from
https://api.github.com/repos/xaif/r2Vault/releases/latest
- Sets Accept header to
application/vnd.github+json for GitHub API v3
- Decodes the JSON response into a
GitHubRelease object
- Strips leading “v” from tag name (e.g., “v1.2.3” → “1.2.3”)
- Compares versions numerically (1.2.3 < 1.10.0)
- Returns release if newer,
nil otherwise
API URL
private static let apiURL = URL(string: "https://api.github.com/repos/xaif/r2Vault/releases/latest")!
Points to the GitHub Releases API for the r2Vault repository.
GitHub Data Types
GitHubRelease
Represents a GitHub release.
struct GitHubRelease: Decodable {
let tagName: String
let htmlUrl: String
let body: String?
let assets: [GitHubAsset]
var dmgDownloadURL: URL?
}
Release version tag (e.g., "v1.2.3").
URL to the release page on GitHub.
Release notes in Markdown format.
Array of release assets (downloadable files).
Computed property returning the download URL for the first .dmg asset.
dmgDownloadURL
var dmgDownloadURL: URL? {
assets.first(where: { $0.name.hasSuffix(".dmg") }).flatMap { URL(string: $0.browserDownloadUrl) }
}
Finds the first DMG asset and returns its download URL.
GitHubAsset
Represents a release asset file.
struct GitHubAsset: Decodable {
let name: String
let browserDownloadUrl: String
}
Asset filename (e.g., "R2Vault-1.2.3.dmg").
Direct download URL for the asset.
AppUpdater Integration
For complete documentation on the DMG download and installation process, see AppUpdater.
The AppUpdater handles:
- Downloading DMG files from GitHub releases
- Progress tracking during download
- Mounting and installing updates
- Safety checks for writable locations
- Automatic app replacement and relaunch
Usage Examples
Check for Updates
import SwiftUI
struct UpdateCheckView: View {
@State private var availableUpdate: GitHubRelease?
@State private var isChecking = false
@State private var errorMessage: String?
var body: some View {
VStack {
if isChecking {
ProgressView("Checking for updates...")
} else if let update = availableUpdate {
Text("Update available: \(update.tagName)")
if let notes = update.body {
Text(notes)
.font(.caption)
}
Button("Install Update") {
AppUpdater.shared.install(release: update)
}
} else {
Text("You're up to date!")
}
if let error = errorMessage {
Text("Error: \(error)")
.foregroundStyle(.red)
}
}
.task {
await checkForUpdate()
}
}
func checkForUpdate() async {
isChecking = true
errorMessage = nil
defer { isChecking = false }
do {
availableUpdate = try await UpdateService.checkForUpdate()
} catch {
errorMessage = error.localizedDescription
}
}
}
Download and Install
struct UpdaterView: View {
@State private var updater = AppUpdater.shared
var body: some View {
VStack {
switch updater.state {
case .idle:
Text("Ready to update")
case .downloading(let progress):
ProgressView(value: progress) {
Text("Downloading: \(Int(progress * 100))%")
}
Button("Cancel") {
updater.cancel()
}
case .downloaded:
Text("Update ready to install")
Button("Install Now") {
updater.installDownloaded()
}
.buttonStyle(.borderedProminent)
case .installing:
ProgressView("Installing...")
Text("The app will restart automatically")
.font(.caption)
case .failed(let message):
Text("Update failed: \(message)")
.foregroundStyle(.red)
Button("Try Again") {
// Retry logic
}
}
}
.padding()
}
}
Automatic Updates
@main
struct R2VaultApp: App {
@State private var viewModel = AppViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.task {
// Check for updates on launch
await checkForUpdatesOnLaunch()
}
}
}
func checkForUpdatesOnLaunch() async {
// Wait a bit before checking (don't block app launch)
try? await Task.sleep(for: .seconds(3))
guard AppUpdater.shared.canSelfUpdate else {
print("Self-update not available: \(AppUpdater.shared.updateBlockReason ?? "unknown")")
return
}
do {
if let release = try await UpdateService.checkForUpdate() {
print("Update available: \(release.tagName)")
// Show update notification or prompt
}
} catch {
print("Update check failed: \(error)")
}
}
}
Security Considerations
Update SecurityThe current implementation:
- Uses HTTPS for GitHub API (transport security)
- Removes quarantine attributes (
com.apple.quarantine)
- Does NOT verify code signatures
- Does NOT verify DMG checksums
For production apps, consider:
- Verifying DMG checksums from GitHub release notes
- Checking code signatures before installation
- Using Apple’s Sparkle framework for updates
- Implementing delta updates for bandwidth efficiency
App Store Distribution
If distributing via the Mac App Store:
- Remove the auto-update system entirely
- App Store handles all updates automatically
- Self-updating violates App Store guidelines
- Can still check GitHub API to notify users of updates
- MenuBarManager - Can display update notifications
- GitHub Releases API - Source of update metadata