StableID is a simple package that helps you keep a stable user identifier across devices by leveraging iCloud Key Value Store).
It's useful for services like RevenueCat, where you may want to maintain a consistent user identifier to allow users to access their purchases across their devices, but you don't want to have a complete account system or use anonymous identifiers.
StableID persists across all devices of a user's iCloud account.
Add this repository as a Swift package.
https://github.com/codykerns/StableID
In order to use StableID, you'll need to add the iCloud capability to your target and enable Key-value storage:
Getting the current stable identifier is simple:
let currentID = StableID.idThat's it. One line to get a user identifier that persists across devices and app reinstalls.
For App Store apps, the best way to configure StableID is using the App Store's AppTransactionID. This provides a globally unique, stable identifier tied to each user's Apple Account:
// Only fetch if not already configured
if StableID.hasStoredID {
StableID.configure()
} else {
Task {
let id = try await StableID.fetchAppTransactionID()
StableID.configure(id: id)
}
}Or, use a policy:
Task {
let id = try await StableID.fetchAppTransactionID()
StableID.configure(id: id, policy: .preferStored)
}The .preferStored policy ensures that if an ID is already stored (from another device via iCloud), it will be used instead of the provided ID during the configure call. This keeps your ID consistent across all devices.
Benefits:
- Globally unique per Apple Account
- Persists across redownloads, refunds, and repurchases
- Works even without in-app purchases
- Unique per family member for Family Sharing apps
- Most reliable identifier for App Store distributed apps
- Only fetches from App Store once, then uses stored value
Alternatively, you can initialize StableID with auto-generated identifiers:
StableID.configure()By default, StableID will look for any other StableID identifier in iCloud or local user defaults - otherwise, it will generate a new identifier.
If you want to provide a custom identifier to force the client to be set to a specific identifier and update iCloud:
StableID.configure(id: <optional_user_id>)Call StableID.isConfigured to see if StableID has already been configured.
When providing an ID to configure(), you can specify a policy to control how that ID is used:
.preferStored
- Checks iCloud and local storage first
- Only uses the provided ID if no stored ID exists
- Ensures consistency across app launches
let id = try await StableID.fetchAppTransactionID()
StableID.configure(id: id, policy: .preferStored).forceUpdate (Default)
- Always uses the provided ID
- Updates storage with the new ID
- Use when you want to override any existing stored ID
StableID.configure(id: "user-123", policy: .forceUpdate)To change identifiers, call:
StableID.identify(id: <new_user_identifier>)To receive updates when a user identifier changes (for example from detecting a change from another iCloud device), configure a delegate:
// call after configuring StableID
StableID.set(delegate: MyClass())
class MyClass: StableIDDelegate {
func willChangeID(currentID: String, candidateID: String) -> String? {
// called before StableID changes IDs, it gives you the option to return the proper ID
}
func didChangeID(newID: String) {
// called once the ID changes
}
}By default, StableID uses a standard IDGenerator that generates simple UUIDs.
If you want any generated identifiers to follow a certain pattern, you can implement a custom ID generator by conforming to IDGenerator and implementing generateID():
struct MyCustomIDGenerator: IDGenerator {
func generateID() -> String {
// do something custom
return myGeneratedID
}
}Then pass the generator as part of the configure method:
StableID.configure(idGenerator: MyCustomIDGenerator())Built-in generators
StableID.StandardGenerator: Standard UUIDsStableID.ShortIDGenerator: 8-character alphanumeric IDs
Example 1: Basic Setup with RevenueCat
Configure StableID and use it to configure RevenueCat with a consistent user identifier:
import StableID
import RevenueCat
class AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Configure StableID first
if StableID.hasStoredID {
StableID.configure()
// Configure RevenueCat with StableID
Purchases.configure(withAPIKey: "your_api_key", appUserID: StableID.id)
} else {
Task {
// Try to fetch AppTransactionID, fallback to generated ID if it fails
if let id = try? await StableID.fetchAppTransactionID() {
StableID.configure(id: id)
} else {
StableID.configure()
}
// Configure RevenueCat after StableID is ready
Purchases.configure(withAPIKey: "your_api_key", appUserID: StableID.id)
}
}
return true
}
}Example 2: Handling ID Changes with RevenueCat
Use the delegate pattern to update RevenueCat when the StableID changes (e.g., from another device via iCloud):
import StableID
import RevenueCat
class AppCoordinator: StableIDDelegate {
init() {
// Set up StableID delegate
StableID.set(delegate: self)
}
func willChangeID(currentID: String, candidateID: String) -> String? {
// Optional: validate or modify the candidate ID
return nil
}
func didChangeID(newID: String) {
// Update RevenueCat with the new ID
Purchases.shared.logIn(newID) { customerInfo, created, error in
if let error = error {
print("Error updating RevenueCat user: \(error)")
} else {
print("Successfully updated RevenueCat user to: \(newID)")
}
}
}
}Example 3: User Login Flow
Handle the case where a user logs into your app with their own account:
import StableID
import RevenueCat
func userDidLogin(userID: String) {
// Update StableID to use the user's account ID
StableID.identify(id: userID)
// Update RevenueCat to match
Purchases.shared.logIn(userID) { customerInfo, created, error in
if let error = error {
print("Error logging in to RevenueCat: \(error)")
} else {
print("Successfully logged in to RevenueCat")
}
}
}
func userDidLogout() {
// Generate a new anonymous ID
StableID.generateNewID()
// Switch RevenueCat to the new anonymous ID
Purchases.shared.logIn(StableID.id) { customerInfo, created, error in
if let error = error {
print("Error switching to anonymous ID: \(error)")
} else {
print("Switched to anonymous ID: \(StableID.id)")
}
}
}Example 4: SwiftUI App with Async Configuration
For SwiftUI apps, configure StableID and RevenueCat in your App struct:
import SwiftUI
import StableID
import RevenueCat
@main
struct MyApp: App {
init() {
// Configure StableID with .preferStored policy
Task {
do {
let id = try await StableID.fetchAppTransactionID()
StableID.configure(id: id, policy: .preferStored)
// Configure RevenueCat after StableID is ready
Purchases.configure(withAPIKey: "your_api_key", appUserID: StableID.id)
} catch {
print("Error configuring StableID: \(error)")
// Fallback to generated ID
StableID.configure()
Purchases.configure(withAPIKey: "your_api_key", appUserID: StableID.id)
}
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Example 5: Custom ID Generator for Testing
Use a custom ID generator for testing or specific formatting requirements:
import StableID
struct TestIDGenerator: IDGenerator {
func generateID() -> String {
return "test-user-\(UUID().uuidString.prefix(8))"
}
}
#if DEBUG
StableID.configure(idGenerator: TestIDGenerator())
#else
StableID.configure()
#endifMIT