Skip to content

A simple, stable user identifier across iOS devices using iCloud Key Value Store. Supports iOS, macOS, watchOS, tvOS, and visionOS.

License

Notifications You must be signed in to change notification settings

codykerns/StableID

Repository files navigation

A simple, stable user identifier across devices

SwiftPM compatible

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.

πŸ“¦ Installation

Add this repository as a Swift package.

https://github.com/codykerns/StableID

ℹ️ Before using StableID

In order to use StableID, you'll need to add the iCloud capability to your target and enable Key-value storage:

Screenshot 2024-02-17 at 1 12 04β€―AM

🀩 The pitch: a single point to get a consistent ID

Getting the current stable identifier is simple:

let currentID = StableID.id

That's it. One line to get a user identifier that persists across devices and app reinstalls.

πŸ› οΈ Configuration

Recommended: Use App Store Transaction ID (iOS 16.0+)

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

Basic Configuration

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.

ID Policies

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)

Changing identifiers

To change identifiers, call:

StableID.identify(id: <new_user_identifier>)

Receiving updates

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
    }
}

Custom ID Generators

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 UUIDs
  • StableID.ShortIDGenerator: 8-character alphanumeric IDs

πŸ“š Examples

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()
#endif

πŸ“™ License

MIT

About

A simple, stable user identifier across iOS devices using iCloud Key Value Store. Supports iOS, macOS, watchOS, tvOS, and visionOS.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages