Easily stop users from keeping old versions of your app around for a while.
Remember the days when you got software on a disk and installed it to your computer manually? Mobile, unfortunately, is still a little like that. Once the code is out there… it's out there. You can't simply revert the branch and redeploy a pipeline. Apple needs to review and approve the changes, then you need to get the user to download it!
If the root view is wrapped by the VersionLockoutView, you don't have to think about version lockout. This will take care of it for you including giving you built in views for when the user should update their app.
First things first, you'll need an endpoint, a JSON file in an S3 bucket, or even a JSON file on GitHub — that returns the lockout information for your app. The shape of that data will look like the following:
{
"recommendedVersion": "2022.08.25",
"requiredVersion": "2022.08.25",
"updateUrl" : "https://apps.apple.com/the-link-to-your-app",
"eol": false,
"message": "this message is optional and is only used for the end of life for the app"
}Next, you will add the package to your app using Swift Package Manager (SPM).
To add this package to your app, use Swift Package Manager. Be sure to add VersionLockout to your app target when you add the dependency.
Once the dependency has been added to your project, you can import VersionLockout.
Then wrap your outter most view with the VersionLockoutView and pass in the link to the settings file like in the following example:
import SwiftUI
import VersionLockout // ADD: this import
@main
struct ExampleApp: App {
// ADD: VersionLockoutViewModel to your view
@State var versionLockoutVM = VersionLockoutViewModel(URL(string: "https://github.com/link-to-my-version-data.json")!)
var body: some Scene {
WindowGroup {
// WRAP: your main view with VersionLockoutView and pass the view model
VersionLockoutView(viewModel: versionLockoutVM) {
ContentView()
}
}
}
}- Recommended update: Gives the user the ability to skip updating their app for a short time (uses the task modifier to check for updates and reminds them).
- Required update: Prevents the user from interacting with the app until the update has been completed.
- EOL Update: Currently, the End Of Life (EOL) option is only useful on the Android side (since the App Store allows you to remove apps from a users device and the Play Store does not). The message parameter is currently only used for this screen.
| Recommended update | Required update | End of Life |
|---|---|---|
![]() |
![]() |
![]() |
By default, VersionLockout re-checks the version API every 3 hours when your app returns to the foreground. This ensures long-running apps (e.g., apps that stay open for days) don't miss important version updates.
How it works:
- On initial launch, the version check runs immediately
- The last fetch timestamp is stored in UserDefaults
- When the app returns to the foreground (
scenePhasebecomes.active), VersionLockout checks if 3 hours have elapsed since the last fetch - If the interval has passed, a new fetch is triggered automatically
Custom refresh interval:
The refreshInterval parameter accepts any Measurement<UnitDuration>, giving you flexibility in how you specify the interval:
// 1 hour
@State var vm = VersionLockoutViewModel(
URL(string: "https://github.com/link-to-my-version-data.json")!,
refreshInterval: .init(value: 1, unit: .hours)
)
// 30 minutes
@State var vm = VersionLockoutViewModel(
URL(string: "https://github.com/link-to-my-version-data.json")!,
refreshInterval: .init(value: 30, unit: .minutes)
)Disable automatic refresh: To effectively disable automatic refresh (check only on launch), set a very large interval:
@State var versionLockoutVM = VersionLockoutViewModel(
URL(string: "https://github.com/link-to-my-version-data.json")!,
refreshInterval: .init(value: 1, unit: .days) // Check once per day
)Loading state on refresh:
By default, the loading state (isLoading) only shows on the initial load when no status exists yet. Subsequent refreshes happen silently in the background, keeping the current content visible. If you want to show a loading indicator on every refresh:
@State var versionLockoutVM = VersionLockoutViewModel(
URL(string: "https://github.com/link-to-my-version-data.json")!,
showLoadingOnRefresh: true
)If you want to display your own view for any status, then the code would look like the following example:
import SwiftUI
import VersionLockout // ADD: this import
@main
struct ExampleApp: App {
// ADD: VersionLockoutViewModel to your view
@State var versionLockoutVM = VersionLockoutViewModel(URL(string: "https://github.com/link-to-my-version-data.json")!)
var body: some Scene {
WindowGroup {
// Example of completely custom views for every status
VersionLockoutView(viewModel: versionLockoutVM) {
Text("I'm Loading")
} updateRecommended: { _, _ in
Text("Recomend")
} updateRequred: { _ in
Text("Required")
} endOfLife: { _ in
Text("I'm EOL")
} upToDate: {
// Your normal app view goes here
Text("I'm up to date")
}
}
}
}Each custom view closure receives parameters that you can use to build your UI:
| Closure | Parameters | Description |
|---|---|---|
ifLoading |
None | Shown while fetching version data |
updateRecommended |
updateUrl: URL, skip: () -> Void |
The updateUrl is the App Store link from your JSON. Call skip() to let the user dismiss the recommendation temporarily. |
updateRequred |
updateUrl: URL |
The updateUrl is the App Store link from your JSON. No skip option since the update is mandatory. |
endOfLife |
message: String? |
The optional message from your JSON to display to the user. |
upToDate |
None | Your main app content goes here |
Here's an example showing how to use these parameters in your custom views:
struct MyCustomUpdateRecommendedView: View {
let updateUrl: URL
let skip: () -> Void
@Environment(\.openURL) private var openURL
var body: some View {
VStack {
Text("A new version is available!")
Button("Update Now") {
openURL(updateUrl)
}
Button("Remind Me Later") {
skip() // Dismisses this view and shows the main content
}
}
}
}
struct MyCustomUpdateRequiredView: View {
let updateUrl: URL
@Environment(\.openURL) private var openURL
var body: some View {
VStack {
Text("Please update to continue using the app")
Button("Update Now") {
openURL(updateUrl)
}
// No skip option - update is required
}
}
}
struct MyCustomEOLView: View {
let message: String?
var body: some View {
VStack {
Text("This app is no longer supported")
if let message = message {
Text(message)
}
}
}
}Then use your custom views in the VersionLockoutView:
VersionLockoutView(viewModel: versionLockoutVM) {
// Loading - no parameters
ProgressView("Checking for updates...")
} updateRecommended: { updateUrl, skip in
MyCustomUpdateRecommendedView(updateUrl: updateUrl, skip: skip)
} updateRequred: { updateUrl in
MyCustomUpdateRequiredView(updateUrl: updateUrl)
} endOfLife: { message in
MyCustomEOLView(message: message)
} upToDate: {
// No parameters - your main app content
ContentView()
}

