Skip to main content

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

enum UpdateService
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:
  1. Fetches the latest release from https://api.github.com/repos/xaif/r2Vault/releases/latest
  2. Sets Accept header to application/vnd.github+json for GitHub API v3
  3. Decodes the JSON response into a GitHubRelease object
  4. Strips leading “v” from tag name (e.g., “v1.2.3” → “1.2.3”)
  5. Compares versions numerically (1.2.3 < 1.10.0)
  6. 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?
}
tagName
String
Release version tag (e.g., "v1.2.3").
htmlUrl
String
URL to the release page on GitHub.
body
String?
Release notes in Markdown format.
assets
[GitHubAsset]
Array of release assets (downloadable files).
dmgDownloadURL
URL?
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
}
name
String
Asset filename (e.g., "R2Vault-1.2.3.dmg").
browserDownloadUrl
String
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