Clear stale Keychain sessions after reinstall in Swift
Reinstalling an iOS app clears its UserDefaults while its Keychain session
survives. The startup guard does four things:
- Return when the launch marker exists.
- Preserve the session when other defaults show that this is an update.
- If the defaults domain is empty, delete the matching Keychain item.
- Write the marker after any deletion succeeds. Then restore the session.
import Foundation
import Security
enum ReinstallCleanupError: Error {
case missingBundleIdentifier
case keychain(OSStatus)
}
func clearStaleSessionAfterReinstall() async throws {
guard let domainName = Bundle.main.bundleIdentifier else {
throw ReinstallCleanupError.missingBundleIdentifier
}
let marker = "hasLaunchedBefore"
let defaults = UserDefaults.standard
let domain = defaults.persistentDomain(forName: domainName) ?? [:]
guard domain[marker] as? Bool != true else { return }
// An empty domain means a possible fresh install or reinstall.
if domain.isEmpty {
let status = await Task.detached(priority: .userInitiated) {
SecItemDelete([
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.myapp.session",
// Add kSecAttrAccount when this service stores multiple accounts.
] as CFDictionary)
}.value
guard status == errSecSuccess || status == errSecItemNotFound else {
throw ReinstallCleanupError.keychain(status)
}
}
// A failed deletion throws before this line, leaving cleanup retryable.
defaults.set(true, forKey: marker)
}
Reserve the example Keychain service for session data.
At startup:
func bootstrapAuthentication(
restoreSession: () async throws -> Void
) async throws {
try await clearStaleSessionAfterReinstall()
try await restoreSession()
}
Run it before other startup code writes defaults. In an existing app, a missing marker can also mean the first launch after an update. The empty domain check is safe only when every existing installation already writes another default. Prefer a known legacy key. If that guarantee is unavailable, seed the marker in an earlier release. Users can skip that release. Delay cleanup until the remaining logout risk is acceptable.
Verify both lifecycle paths: reinstall clears the session and an update keeps it. Also confirm that cleanup finishes before the first session read.
A fresh install takes the same deletion path. errSecItemNotFound does not fail
cleanup.