When a user gets a new iPhone, iCloud can restore app data from a different device, which copies info from UserDefaults and the Keychain.
This presents problems for my app when a user migrates from iPhone A -> iPhone B, because the app stores a device-specific security key that changes irregularly.
This would be easy to fix if I could detect the iCloud data restore, or an upgrade to a new device. This would allow me to reset the persisted device identifier and clear out the persisted old security key.
But I can find no way to do so, because Apple blocks accessing any unique device identifier so you can't tell if the app has moved to a new device. It also gives no callbacks about when an iCloud restore happened. I could check the hardware device model for changes, but sometimes a user replaces a phone with identical hardware when a phone is damaged or lost.
Is there any way to detect migration of an app to a new device and/or prevent cloning of iCloud backups of my app data from one device to another?
You can detect if an app is installed from iCloud backup by saving a file in the .applicationSupportDirectory
. That directory is not backed up, so if your app crates a file there and doesn't see it, then that means it is (a) the first time your app has run or (b) the app was restored from backup.
You can use this as a flag to perform any special cleanup when a restore is detected.
And if you need to discern between a first time install and a restore, just save a second flag to UserDefaults
. If the flag exists in UserDefaults
but the flag file does not exist in .applicationSupportDirectory
then you know it was an iCloud restore.
This technique has passed App Store review once as of this writing.
class RestoredAppDetector {
func saveInstallationFlagFile() {
if let applicationSupportDirectory = try? FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
var flagFile = applicationSupportDirectory.appendingPathComponent("app_installed.txt", isDirectory: false)
if (!FileManager.default.createFile(atPath: flagFile.path, contents: "true".data(using: .utf8)) ) {
NSLog("Filed to create flag file")
}
var values = URLResourceValues()
values.isExcludedFromBackup = true
do {
try flagFile.setResourceValues(values)
}
catch {
NSLog("Failed to set resource value")
}
}
else {
NSLog("Could not create application support directory.")
}
}
func installationFlagFileExists() -> Bool {
if let applicationSupportDirectory = try? FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false) {
let flagFile = applicationSupportDirectory.appendingPathComponent("app_installed.txt", isDirectory: false)
if (FileManager.default.fileExists(atPath: flagFile.path)) {
NSLog("Flag file exists")
return true
}
else {
NSLog("Flag file does not exist")
}
}
else {
NSLog("Could not find application support directory.")
}
return false
}
}