
Force Update
- 3 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates a minimum-version enforcement system for iOS/macOS with hard-block and soft-prompt update flows, App Store redirect, and remote-config or App Store lookup version checks.
About
Generates a Swift minimum-version check that hard-blocks or soft-prompts users to update, redirects to the App Store, and reads versions from remote config or the App Store lookup API. A developer uses it to force or recommend app updates for old clients.
- Hard-block and dismissible soft-prompt update flows
- Version source via remote config or App Store lookup API
Force Update by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #887 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill force-updateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates a minimum-version enforcement system for iOS/macOS with hard-block and soft-prompt update flows, App Store redirect, and remote-config or App Store lookup version checks.
Files
Force Update Generator
Generate a minimum version check system that blocks app usage when a critical update is required, or shows a dismissible prompt for recommended updates. Checks the current app version against a remote configuration endpoint or the App Store lookup API and presents the appropriate UI.
When This Skill Activates
Use this skill when the user:
- Asks to "add force update" or "force update screen"
- Wants "minimum version check" or "minimum version enforcement"
- Mentions "required update" or "mandatory update"
- Asks about "version check" or "app version check"
- Wants an "app update prompt" or "update dialog"
- Mentions "block old versions" or "deprecate old versions"
Pre-Generation Checks
1. Project Context Detection
- [ ] Check Swift version (requires Swift 5.9+)
- [ ] Check deployment target (iOS 16+ / macOS 13+)
- [ ] Check for @Observable support (iOS 17+ / macOS 14+)
- [ ] Identify source file locations
2. Conflict Detection
Search for existing version check code:
Glob: **/*ForceUpdate*.swift, **/*VersionCheck*.swift, **/*UpdateManager*.swift, **/*AppVersion*.swift
Grep: "ForceUpdate" or "minimumVersion" or "mandatoryUpdate" or "CFBundleShortVersionString"If an existing update mechanism is found:
- Ask if user wants to replace or extend it
- If extending, integrate with the existing check flow
3. Network Layer Detection
Search for existing networking code:
Glob: **/*API*.swift, **/*Client*.swift, **/*Network*.swift
Grep: "APIClient" or "URLSession" or "NetworkService"If a networking layer exists, generate the version checker to use it rather than raw URLSession.
Configuration Questions
Ask user via AskUserQuestion:
1. Update check source?
- Remote JSON endpoint (your own server hosts a JSON config) — recommended
- App Store lookup API (uses iTunes Search API, no server needed)
- Firebase Remote Config (requires Firebase SDK)
2. Update types to support?
- Hard block only (always force update)
- Soft prompt only (always dismissible)
- Both hard block and soft prompt — recommended
3. Check frequency?
- Every app launch
- Once per day — recommended
- Once per week
4. Include skip option for soft updates?
- Yes — user can skip a specific version and won't be prompted again until a newer version is available
- No — prompt shows every time (respecting check frequency)
Generation Process
Step 1: Read Templates
Read templates.md for production Swift code.
Step 2: Create Core Files
Generate these files: 1. AppVersion.swift — Semantic version model (major.minor.patch) with Comparable 2. UpdateRequirement.swift — Enum: none, softUpdate, hardUpdate with message and store URL 3. VersionChecker.swift — Protocol + implementation based on chosen source
Step 3: Create Manager
4. UpdateManager.swift — @Observable manager that coordinates checks, caches timing, exposes state
Step 4: Create UI Files
5. ForceUpdateView.swift — Full-screen blocking view with App Store button 6. SoftUpdateBannerView.swift — Dismissible banner with Update and Later buttons
Step 5: Create Integration
7. UpdateCheckModifier.swift — ViewModifier that auto-checks on appear and presents UI
Step 6: Determine File Location
Check project structure:
- If
Sources/exists →Sources/ForceUpdate/ - If
App/exists →App/ForceUpdate/ - Otherwise →
ForceUpdate/
Output Format
After generation, provide:
Files Created
ForceUpdate/
├── AppVersion.swift # Semantic version model with Comparable
├── UpdateRequirement.swift # none / softUpdate / hardUpdate enum
├── VersionChecker.swift # Protocol + remote/App Store implementation
├── UpdateManager.swift # @Observable coordinator
├── ForceUpdateView.swift # Full-screen blocking UI
├── SoftUpdateBannerView.swift # Dismissible banner UI
└── UpdateCheckModifier.swift # ViewModifier for auto-checkIntegration Steps
Add the modifier to your root view:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.checkForUpdates()
}
}
}Custom configuration:
ContentView()
.checkForUpdates(
checker: RemoteJSONVersionChecker(
url: URL(string: "https://api.example.com/app-config")!
),
frequency: .daily
)Manual check from a settings screen:
struct SettingsView: View {
@Environment(UpdateManager.self) private var updateManager
var body: some View {
Section("App") {
if case .softUpdate(_, let message) = updateManager.requirement {
Button("Update Available") {
updateManager.openAppStore()
}
}
Button("Check for Updates") {
Task { await updateManager.checkNow() }
}
}
}
}Testing
@Test
func hardUpdateBlocksWhenBelowMinimum() async throws {
let checker = MockVersionChecker(
requirement: .hardUpdate(
version: AppVersion(major: 2, minor: 0, patch: 0),
message: "Critical security fix"
)
)
let manager = UpdateManager(
checker: checker,
currentVersion: AppVersion(major: 1, minor: 0, patch: 0)
)
await manager.checkNow()
guard case .hardUpdate = manager.requirement else {
Issue.record("Expected hard update requirement")
return
}
}
@Test
func softUpdateAllowsSkip() async throws {
let checker = MockVersionChecker(
requirement: .softUpdate(
version: AppVersion(major: 1, minor: 5, patch: 0),
message: "New features available"
)
)
let manager = UpdateManager(
checker: checker,
currentVersion: AppVersion(major: 1, minor: 0, patch: 0)
)
await manager.checkNow()
manager.skipCurrentUpdate()
#expect(manager.requirement == .none)
}
@Test
func respectsCheckFrequency() async throws {
let checker = MockVersionChecker(requirement: .none)
let manager = UpdateManager(checker: checker, frequency: .daily)
await manager.checkNow()
#expect(checker.checkCount == 1)
// Second check within frequency window should skip
await manager.checkIfNeeded()
#expect(checker.checkCount == 1)
}Common Patterns
Check on Every Launch
// In your App's root view
ContentView()
.checkForUpdates(frequency: .everyLaunch)Hard Block for Critical Security Updates
The remote JSON should differentiate between hard and soft:
{
"minimumVersion": "2.0.0",
"minimumVersionMessage": "This version is no longer supported. Please update for security fixes.",
"recommendedVersion": "2.1.0",
"recommendedVersionMessage": "Update for new features and improvements.",
"storeURL": "https://apps.apple.com/app/id123456789"
}Soft Prompt with Skip
SoftUpdateBannerView(
message: "A new version is available",
onUpdate: { updateManager.openAppStore() },
onSkip: { updateManager.skipCurrentUpdate() }
)Gotchas
App Store Lookup API Rate Limits
The iTunes Search API (itunes.apple.com/lookup?bundleId=...) has undocumented rate limits. For high-volume apps, prefer a remote JSON endpoint you control. Cache the response and avoid calling on every cold launch.
Version String Comparison
Never compare version strings lexicographically — "9.0.0" > "10.0.0" evaluates to true. Always parse into semantic version components (major, minor, patch) and compare numerically.
Don't Block During Onboarding
If the user hasn't completed onboarding, defer the force update check until after. Blocking a first-launch user with an update screen creates confusion.
Offline Handling
If the version check network request fails, do not block the user. Default to .none (allow usage). Only block when you have a confirmed response that the version is below minimum.
TestFlight and Debug Builds
Skip force update checks for TestFlight and debug builds. TestFlight builds always have a higher build number but may have a lower marketing version during development:
#if DEBUG
// Skip version check
#else
// Check version
#endifApp Review Rejection Risk
Apple may reject apps that show an update prompt during review if the current App Store version is the one under review. Consider disabling the check when the app is running in a sandbox/review environment.
References
- templates.md — All production Swift code templates
- Related:
generators/networking-layer— Base networking layer for API calls - Related:
generators/feature-flags— Feature flags can control update enforcement - Related:
generators/whats-new— Show what's new after an update completes
Force Update Code Templates
Production-ready Swift templates for a force update / minimum version check system. All code targets iOS 16+ / macOS 13+ (iOS 17+ / macOS 14+ for @Observable) and uses modern Swift concurrency.
AppVersion.swift
import Foundation
/// Semantic version representation (major.minor.patch) with comparison support.
///
/// Parses version strings like "2.1.3" and provides `Comparable` conformance
/// for safe numeric comparison — never compare version strings lexicographically.
///
/// Usage:
/// ```swift
/// let current = AppVersion.current // From Bundle
/// let minimum = AppVersion(string: "2.0.0")
/// if current < minimum { /* force update */ }
/// ```
struct AppVersion: Sendable, Codable, Hashable {
let major: Int
let minor: Int
let patch: Int
init(major: Int, minor: Int, patch: Int) {
self.major = major
self.minor = minor
self.patch = patch
}
/// Parse from a version string like "2.1.3".
/// Returns nil if the string cannot be parsed.
init?(string: String) {
let components = string
.trimmingCharacters(in: .whitespacesAndNewlines)
.split(separator: ".")
.compactMap { Int($0) }
guard components.count >= 2 else { return nil }
self.major = components[0]
self.minor = components[1]
self.patch = components.count >= 3 ? components[2] : 0
}
/// The current app version from the main bundle's CFBundleShortVersionString.
static var current: AppVersion {
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
return AppVersion(string: versionString) ?? AppVersion(major: 0, minor: 0, patch: 0)
}
/// Display string, e.g. "2.1.3".
var displayString: String {
"\(major).\(minor).\(patch)"
}
}
extension AppVersion: Comparable {
static func < (lhs: AppVersion, rhs: AppVersion) -> Bool {
if lhs.major != rhs.major { return lhs.major < rhs.major }
if lhs.minor != rhs.minor { return lhs.minor < rhs.minor }
return lhs.patch < rhs.patch
}
}
extension AppVersion: CustomStringConvertible {
var description: String { displayString }
}UpdateRequirement.swift
import Foundation
/// The result of a version check — determines what UI to show.
///
/// - `none`: App is up to date, no action needed.
/// - `softUpdate`: A recommended update is available. User can dismiss or skip.
/// - `hardUpdate`: A mandatory update is required. App usage is blocked.
enum UpdateRequirement: Sendable, Codable, Equatable {
/// App is current or check was skipped.
case none
/// A recommended (non-blocking) update is available.
case softUpdate(version: AppVersion, message: String)
/// A mandatory (blocking) update is required.
case hardUpdate(version: AppVersion, message: String)
/// The App Store URL for this app.
///
/// Set this from the remote config response.
/// Falls back to the main App Store page if not configured.
var storeURL: URL {
// Replace with your actual App Store URL or make configurable.
URL(string: "https://apps.apple.com/app/id000000000")!
}
}
/// Remote configuration model for JSON-based version checking.
///
/// Expected JSON format:
/// ```json
/// {
/// "minimumVersion": "2.0.0",
/// "minimumVersionMessage": "Please update for critical security fixes.",
/// "recommendedVersion": "2.1.0",
/// "recommendedVersionMessage": "Update for new features and improvements.",
/// "storeURL": "https://apps.apple.com/app/id123456789"
/// }
/// ```
struct RemoteVersionConfig: Sendable, Codable {
let minimumVersion: String
let minimumVersionMessage: String
let recommendedVersion: String?
let recommendedVersionMessage: String?
let storeURL: String?
/// Evaluate the update requirement for a given app version.
func requirement(for currentVersion: AppVersion) -> UpdateRequirement {
// Check hard block first
if let minimum = AppVersion(string: minimumVersion), currentVersion < minimum {
return .hardUpdate(version: minimum, message: minimumVersionMessage)
}
// Check soft recommendation
if let recommendedString = recommendedVersion,
let recommended = AppVersion(string: recommendedString),
let message = recommendedVersionMessage,
currentVersion < recommended {
return .softUpdate(version: recommended, message: message)
}
return .none
}
}VersionChecker.swift
import Foundation
/// Protocol for checking the minimum required app version.
///
/// Conform to this protocol to implement custom version check sources
/// (remote JSON, App Store lookup, Firebase Remote Config, etc.).
protocol VersionChecking: Sendable {
func checkForUpdate(currentVersion: AppVersion) async throws -> UpdateRequirement
}
// MARK: - Remote JSON Version Checker
/// Checks app version against a JSON endpoint you control.
///
/// This is the recommended approach — you have full control over the response
/// format, rate limits, and can update requirements without an app release.
///
/// Expected JSON format: see `RemoteVersionConfig`.
///
/// Usage:
/// ```swift
/// let checker = RemoteJSONVersionChecker(
/// url: URL(string: "https://api.example.com/app-config")!
/// )
/// let requirement = try await checker.checkForUpdate(currentVersion: .current)
/// ```
actor RemoteJSONVersionChecker: VersionChecking {
private let url: URL
private let session: URLSession
init(url: URL, session: URLSession = .shared) {
self.url = url
self.session = session
}
func checkForUpdate(currentVersion: AppVersion) async throws -> UpdateRequirement {
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw VersionCheckError.serverError
}
let decoder = JSONDecoder()
let config = try decoder.decode(RemoteVersionConfig.self, from: data)
return config.requirement(for: currentVersion)
}
}
// MARK: - App Store Lookup Checker
/// Checks app version against the App Store using the iTunes Search API.
///
/// No server infrastructure needed — queries Apple's public API.
///
/// **Limitations:**
/// - Undocumented rate limits (avoid calling too frequently)
/// - Only detects that a newer version exists, cannot distinguish hard vs soft
/// - Propagation delay: new versions may not appear immediately after release
/// - All updates are treated as soft updates; override `isCritical` for hard blocks
///
/// Usage:
/// ```swift
/// let checker = AppStoreLookupChecker(bundleId: "com.example.myapp")
/// let requirement = try await checker.checkForUpdate(currentVersion: .current)
/// ```
actor AppStoreLookupChecker: VersionChecking {
private let bundleId: String
private let countryCode: String
private let session: URLSession
init(
bundleId: String? = nil,
countryCode: String = "us",
session: URLSession = .shared
) {
self.bundleId = bundleId ?? Bundle.main.bundleIdentifier ?? ""
self.countryCode = countryCode
self.session = session
}
func checkForUpdate(currentVersion: AppVersion) async throws -> UpdateRequirement {
guard !bundleId.isEmpty else {
throw VersionCheckError.invalidBundleId
}
let url = URL(string: "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=\(countryCode)")!
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw VersionCheckError.serverError
}
let result = try JSONDecoder().decode(AppStoreLookupResponse.self, from: data)
guard let storeVersion = result.results.first?.version,
let latestVersion = AppVersion(string: storeVersion) else {
return .none
}
if currentVersion < latestVersion {
return .softUpdate(
version: latestVersion,
message: "A new version (\(latestVersion.displayString)) is available on the App Store."
)
}
return .none
}
}
/// Response model for the iTunes Search API lookup endpoint.
private struct AppStoreLookupResponse: Codable {
let resultCount: Int
let results: [AppStoreLookupResult]
}
private struct AppStoreLookupResult: Codable {
let version: String
let trackViewUrl: String?
}
// MARK: - Errors
/// Errors that can occur during version checking.
enum VersionCheckError: Error, LocalizedError {
case serverError
case invalidBundleId
case invalidResponse
var errorDescription: String? {
switch self {
case .serverError:
return "Failed to reach the version check server."
case .invalidBundleId:
return "Bundle identifier is missing or invalid."
case .invalidResponse:
return "Received an invalid response from the version check server."
}
}
}UpdateManager.swift
import Foundation
import SwiftUI
/// How often the app should check for updates.
enum UpdateCheckFrequency: Sendable {
case everyLaunch
case daily
case weekly
var interval: TimeInterval {
switch self {
case .everyLaunch: return 0
case .daily: return 24 * 3600
case .weekly: return 7 * 24 * 3600
}
}
}
/// Coordinates version checks, caches timing, and exposes the current update requirement.
///
/// Usage:
/// ```swift
/// let manager = UpdateManager(
/// checker: RemoteJSONVersionChecker(url: configURL),
/// frequency: .daily
/// )
/// await manager.checkIfNeeded()
///
/// switch manager.requirement {
/// case .none: // App is current
/// case .softUpdate: // Show banner
/// case .hardUpdate: // Block usage
/// }
/// ```
@Observable
final class UpdateManager: @unchecked Sendable {
/// The current update requirement after the last check.
private(set) var requirement: UpdateRequirement = .none
/// Whether a check is currently in progress.
private(set) var isChecking = false
private let checker: any VersionChecking
private let currentVersion: AppVersion
private let frequency: UpdateCheckFrequency
private let userDefaults: UserDefaults
private static let lastCheckDateKey = "ForceUpdate_lastCheckDate"
private static let skippedVersionKey = "ForceUpdate_skippedVersion"
init(
checker: any VersionChecking,
currentVersion: AppVersion = .current,
frequency: UpdateCheckFrequency = .daily,
userDefaults: UserDefaults = .standard
) {
self.checker = checker
self.currentVersion = currentVersion
self.frequency = frequency
self.userDefaults = userDefaults
}
/// Check for updates only if enough time has passed since the last check.
///
/// Respects the configured `frequency`. Use `checkNow()` to force a check.
func checkIfNeeded() async {
guard shouldCheck() else { return }
await checkNow()
}
/// Force an immediate version check, ignoring the frequency throttle.
func checkNow() async {
#if DEBUG
// Skip version check in debug builds to avoid blocking development.
// Remove or adjust this guard if you want to test force update UI.
return
#endif
isChecking = true
defer { isChecking = false }
do {
let result = try await checker.checkForUpdate(currentVersion: currentVersion)
recordCheckTime()
// If the user skipped this specific soft update version, treat as none
if case .softUpdate(let version, _) = result, isVersionSkipped(version) {
requirement = .none
} else {
requirement = result
}
} catch {
// On network failure, default to no requirement.
// Never block the user due to a failed check.
requirement = .none
}
}
/// Mark the current soft update version as skipped.
///
/// The user won't see the soft prompt again until a newer version is available.
func skipCurrentUpdate() {
if case .softUpdate(let version, _) = requirement {
userDefaults.set(version.displayString, forKey: Self.skippedVersionKey)
requirement = .none
}
}
/// Open the App Store page for this app.
func openAppStore() {
let url = requirement.storeURL
#if canImport(UIKit)
UIApplication.shared.open(url)
#elseif canImport(AppKit)
NSWorkspace.shared.open(url)
#endif
}
// MARK: - Private
private func shouldCheck() -> Bool {
guard frequency != .everyLaunch else { return true }
guard let lastCheck = userDefaults.object(forKey: Self.lastCheckDateKey) as? Date else {
return true
}
return Date().timeIntervalSince(lastCheck) >= frequency.interval
}
private func recordCheckTime() {
userDefaults.set(Date(), forKey: Self.lastCheckDateKey)
}
private func isVersionSkipped(_ version: AppVersion) -> Bool {
guard let skippedString = userDefaults.string(forKey: Self.skippedVersionKey),
let skipped = AppVersion(string: skippedString) else {
return false
}
return skipped == version
}
}ForceUpdateView.swift
import SwiftUI
/// Full-screen blocking view shown when a hard (mandatory) update is required.
///
/// Displays an icon, title, message, and an "Update Now" button that opens
/// the App Store. There is no dismiss option — the user must update.
///
/// Usage:
/// ```swift
/// ForceUpdateView(
/// message: "This version is no longer supported.",
/// storeURL: URL(string: "https://apps.apple.com/app/id123456789")!
/// )
/// ```
struct ForceUpdateView: View {
let message: String
let storeURL: URL
var body: some View {
VStack(spacing: 24) {
Spacer()
Image(systemName: "arrow.down.app.fill")
.font(.system(size: 64))
.foregroundStyle(.tint)
.symbolRenderingMode(.hierarchical)
Text("Update Required")
.font(.title)
.fontWeight(.bold)
Text(message)
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
Button(action: openStore) {
Text("Update Now")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding(.vertical, 4)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.padding(.horizontal, 48)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.interactiveDismissDisabled()
}
private func openStore() {
#if canImport(UIKit)
UIApplication.shared.open(storeURL)
#elseif canImport(AppKit)
NSWorkspace.shared.open(storeURL)
#endif
}
}SoftUpdateBannerView.swift
import SwiftUI
/// Non-blocking banner shown when a soft (recommended) update is available.
///
/// Displays a message with "Update" and "Later" buttons.
/// Supports skipping a specific version so the user isn't prompted again
/// until a newer version is released.
///
/// Usage:
/// ```swift
/// SoftUpdateBannerView(
/// message: "A new version is available with performance improvements.",
/// onUpdate: { updateManager.openAppStore() },
/// onSkip: { updateManager.skipCurrentUpdate() }
/// )
/// ```
struct SoftUpdateBannerView: View {
let message: String
let onUpdate: () -> Void
let onSkip: () -> Void
@State private var isVisible = true
var body: some View {
if isVisible {
VStack(spacing: 12) {
HStack(alignment: .top, spacing: 12) {
Image(systemName: "arrow.down.app")
.font(.title3)
.foregroundStyle(.tint)
VStack(alignment: .leading, spacing: 4) {
Text("Update Available")
.font(.subheadline)
.fontWeight(.semibold)
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Button {
withAnimation {
isVisible = false
}
onSkip()
} label: {
Image(systemName: "xmark")
.font(.caption)
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.accessibilityLabel("Dismiss")
}
HStack(spacing: 12) {
Button("Later") {
withAnimation {
isVisible = false
}
onSkip()
}
.buttonStyle(.bordered)
.controlSize(.small)
Button("Update") {
onUpdate()
}
.buttonStyle(.borderedProminent)
.controlSize(.small)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding()
.background {
RoundedRectangle(cornerRadius: 12)
.fill(Color.secondary.opacity(0.1))
}
.padding(.horizontal)
.transition(.move(edge: .top).combined(with: .opacity))
}
}
}UpdateCheckModifier.swift
import SwiftUI
/// ViewModifier that automatically checks for updates on appear
/// and presents the appropriate UI (hard block or soft banner).
///
/// Usage:
/// ```swift
/// ContentView()
/// .checkForUpdates()
///
/// // With custom configuration:
/// ContentView()
/// .checkForUpdates(
/// checker: RemoteJSONVersionChecker(url: configURL),
/// frequency: .daily
/// )
/// ```
struct UpdateCheckModifier: ViewModifier {
@State private var updateManager: UpdateManager
init(checker: any VersionChecking, frequency: UpdateCheckFrequency) {
_updateManager = State(
wrappedValue: UpdateManager(checker: checker, frequency: frequency)
)
}
func body(content: Content) -> some View {
ZStack {
content
.environment(updateManager)
// Soft update banner at the top
if case .softUpdate(_, let message) = updateManager.requirement {
VStack {
SoftUpdateBannerView(
message: message,
onUpdate: { updateManager.openAppStore() },
onSkip: { updateManager.skipCurrentUpdate() }
)
Spacer()
}
.animation(.easeInOut, value: updateManager.requirement)
}
}
// Hard update as a full-screen cover
.fullScreenCover(
isPresented: Binding(
get: {
if case .hardUpdate = updateManager.requirement { return true }
return false
},
set: { _ in }
)
) {
if case .hardUpdate(_, let message) = updateManager.requirement {
ForceUpdateView(
message: message,
storeURL: updateManager.requirement.storeURL
)
}
}
.task {
await updateManager.checkIfNeeded()
}
}
}
extension View {
/// Check for app updates on appear and present the appropriate UI.
///
/// - Parameters:
/// - checker: The version checking implementation to use.
/// - frequency: How often to check (default: `.daily`).
/// - Returns: A modified view that checks for updates automatically.
func checkForUpdates(
checker: any VersionChecking = RemoteJSONVersionChecker(
url: URL(string: "https://api.example.com/app-config")!
),
frequency: UpdateCheckFrequency = .daily
) -> some View {
modifier(UpdateCheckModifier(checker: checker, frequency: frequency))
}
}