Detect if app starts after backup restore - ios

Is there a way in swift to detect if the app starts after restoring a backup?. Let's say make a backup on a device and restore it into another device.

I tried a solution which seems to work after some testing. I tried several backup restores and it worked fine for me.
The first step would be to save the vendor identifier at the first run of the app:
if UserDefaults.standard.value(forKey: "vendorID") == nil {
if let vendorID = myDevice.identifierForVendor?.description {
UserDefaults.standard.set(vendorID, forKey: "vendorID")
}
}
The second step is to check if the saved vendor id matches the current device's vendor identifier.
Because the vendor id changes after a backup or restoring a backup to a device, I can do other action if the two vendor IDs don't match:
let savedVendorID = UserDefaults.standard.value(forKey: "vendorID") as! String
if savedVendorID == myDevice.identifierForVendor?.description {
//The app can start normally
} else {
//Do action needed after backup restore
}
The vendor id changes under other conditions as well but for my app those changes doesn't matter.

Related

Preventing/Detecting iCloud migration of app data in UserDefaults and KeyChain

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.
The restored security key may be expired (an old backup).
The user may continue using both iPhone A and iPhone B, causing their stored security keys get out-of-sync with rotations.
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
}
}
As far as I know and tested, .applicationSupportDirectory folder is definitely backed up with no problem including all the folders/files hierarchy unless you explicitly excluded some specific items from the backup.

Turning off iCloud and remove items from the ubiquitous container

I have a UISwitch in my app for users to switch on/off iCloud. I want to achieve the following: When a user turns off iCloud, all contents in the ubiquitous container will be removed and copied to a local directory as backups. However, as soon as the files have been removed from the ubiquitous container, copies on the iCloud server are also removed. This basically clear everything on iCloud.
I have the following questions:
How can files on the ubiquitous container be removed without affecting copies on the iCloud server?
What is the best or standard practice to remove files from the ubiquitous container before disabling iCloud?
Can iCloud be disabled at all after it has been initialised/enabled?
Thank you.
After reading Apple's documents and others suggestions, here is my understanding. I am not 100% sure if they are correct. Comments and corrections are most welcome:
Anything added to or removed from the ubiquity container will be synced with the iCloud server. The app has no control of this.
Once iCloud document storage has been enabled in Settings app by the user, it cannot be disabled by the app. The app's responsibility is to provide UI (assuming a UISwitch) to let user indicate where they want their documents synced with the iCloud for the app.
If the user turns off iCloud by turning off the UISwitch in the app (not in Settings), what the app should do is to stop querying metadata, stop listening to NSMetadataQueryDidUpdateNotification, and stop accessing files in the ubiquity container (as mentioned by crizzis above). If later the user turns iCloud on again, files already in the ubiquity container will be synced with iCloud automatically, and no manual merging should be needed unless unresolved conflicts occur.
Using evictUbiquitousItem(at url:)
See point #1. I'm not sure why you would want to do that, though. Can't you just stop accessing the local copies the second the switch is off?
I don't think it can be disabled programmatically. On a positive side, if you want to avail the users of a possibility to disable iCloud, it's already there. iCloud is supposed to be disabled via the Settings app, and all you really need to do is handle that fact within the app by listening to NSUbiquityIdentityDidChangeNotification
UPDATE
Amin Negm-Awad suggested that evictUbiquitousItem(at url:) forces a reload, and so the local copy is not permanently deleted. However, I've done a little testing just out of curiosity, and haven't found that to be the case. The following test:
func runTest(ubiURL: URL) {
self.query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
self.query.predicate = NSPredicate(format: "%K like '*'", NSMetadataItemFSNameKey)
NotificationCenter.default.addObserver(self, selector: #selector(self.metadataQueryDidUpdate(_:)), name: NSNotification.Name.NSMetadataQueryDidUpdate, object: self.query)
self.query.start()
self.query.enableUpdates()
let fileURL = ubiURL.appendingPathComponent("Documents/file.txt")
FileManager.default.createFile(atPath: fileURL.path, contents: "Hello".data(using: .utf8))
do {
try FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
} catch {
print("startDownloadingUbiquitousItem: \(error.localizedDescription)")
}
}
var updateCount = 0
func metadataQueryDidUpdate(_ notification: Notification) {
print("######################")
print("update #\(updateCount)")
for file in query.results as! [NSMetadataItem] {
guard let fileURL = file.value(forAttribute: NSMetadataItemURLKey) as? URL, let fileStatus = file.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String else {
print("Invalid item!")
return
}
if fileStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent {
print("URL to evict: \(fileURL)")
do {
try FileManager.default.evictUbiquitousItem(at: fileURL)
print("Eviction result: successful")
} catch {
print("evictUbiquitousItem: \(error.localizedDescription)")
}
}
print("File exists at URL: \(FileManager.default.fileExists(atPath: fileURL.path))")
}
updateCount = updateCount + 1
}
Yielded:
ubiURL is file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~example~blabla3/
######################
update #0
URL to evict: file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~example~blabla3/Documents/file.txt
evictUbiquitousItem: The file “file.txt” couldn’t be saved in the folder “blabla”.
File exists at URL: true
######################
...
update #3
URL to evict: file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~example~blabla3/Documents/file.txt
Eviction result: successful
File exists at URL: true
######################
update #4
File exists at URL: false
(no further updates followed)
I don't think the remote file will end up on the device unless startDownloadingUbiquitousItemAtUrl: is called with the evicted file's URL. Not sure if this behavior can be relied upon, though.

Executing code after an update

I have a live app that has cached data. When I send out an update on test flight or to the app store I want the app to clear the cached data.
Is there an way to execute a line of code like clearCacheResults() on a update?
You can store the version no in defaults
let version: AnyObject? = NSBundle.mainBundle().infoDictionary!["CFBundleShortVersionString"]
NSUserDefaults.standardUserDefaults().setObject(version, forKey: "appversion")
More info on the bundle keys available here in the doc
First time this will be nil. however on next launch you can retrieve
the previous stored version from defaults
let oldVersion: AnyObject? = NSUserDefaults.standardUserDefaults().objectForKey("appversion")
and compare
if (oldVersion != version) {
// clear cache
}

Disable confirmation on delete request in PHPhotoLibrary

What I am trying to do is to save videos to PHPhotoLibrary, and then remove them when upload to clients remote server in the application completes (basically, photo library serves as temporary storage to add additional layer of security in case anything at all fails (I already save my vides it in the applications directory).
Problem:
The problem is for that to work, everything has to work without input from the user. You can write video to photos library like this:
func storeVideoToLibraryForUpload(upload : SMUpload) {
if PHPhotoLibrary.authorizationStatus() != PHAuthorizationStatus.Authorized {
// Don't write to library since this is disallowed by user
return
}
PHPhotoLibrary.sharedPhotoLibrary().performChanges({ () -> Void in
// Write asset
let assetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(NSURL(fileURLWithPath: upload.nonsecureFilePath!)!)
let assetPlaceholder = assetRequest.placeholderForCreatedAsset
let localIdentifier = assetPlaceholder.localIdentifier
// Store local identifier for later use
upload.localAssetIdentifier = localIdentifier
}, completionHandler: { (success, error) -> Void in
....
})
}
And that works flawlessly, I get local identifier, I store it for later use.. Unicorns and rainbows.
Now when I want to remove that video immediately after upload finishes, I call following:
func removeVideoFromLibraryForUpload(upload : SMUpload) {
// Only proceed if there is asset identifier (video previously stored)
if let assetIdentifier = upload.localAssetIdentifier {
// Find asset that we previously stored
let assets = PHAsset.fetchAssetsWithLocalIdentifiers([assetIdentifier], options: PHFetchOptions())
// Fetch asset, if found, delete it
if let fetchedAssets = assets.firstObject as? PHAsset {
PHPhotoLibrary.sharedPhotoLibrary().performChanges({ () -> Void in
// Delete asset
PHAssetChangeRequest.deleteAssets([fetchedAssets])
}, completionHandler: { (success, error) -> Void in
...
})
}
}
}
Which successfully deletes the video, BUT user have to confirm deletion first. That is a problem as that backing up won't work.
I obviously know why there is confirmation (so you don't clear entire user library for example, but the thing is, My app made the video - and so I thought there will be way around it, since as an "owner" I should not be doing that, or at least have option to disable confirmation.
Thanks in advance!
TLDR: How can I disable confirmation on delete request, if my application created that content? (I don't want to delete anything else).
Note: Somebody can probably say this is rather strange thing to do but the application is distributed internally and there is good reason to do it like this (the video content is too valuable to be lost, even if user deletes the application for some reason, or there is anything at all that goes wrong, we need to be able to preserve the videos), so please don't question that and just focus your attention on the question :)
I cannot see a way to avoid the delete confirmation. It is an implementation detail of the Photos framework, similar to the way you cannot prevent the device from asking the user's permission to use the microphone when your app tries to use it, and is a matter of security & trust. Once you have saved an asset to the device photo library your app is no longer the owner of that asset, so as you noted in your question the device must of course ensure the app has the user's permission before it goes about deleting such data.
You can never entirely safeguard your users' data against their own unpredictable behaviour - if they decide to remove your app, or delete a particular asset from within Photos, it is up to them. I think your best option is to either put up with the built-in delete confirmation, or to provide a guide to your users that makes it clear that they should be careful to protect this important data by backing up their device, and not deleting the app!
If you did decide to stick to this approach, perhaps the best thing you could do is to prepare the user for the fact that their device may ask them for confirmation to delete a file that is being uploaded to your own servers. For example, put up your own modal alert just before trying to delete the asset. I wouldn't normally suggest that kind of approach for a public shipping app, but since you're only distributing internally it may be acceptable for your team.

Delete keychain items when an app is uninstalled

I am using idandersen's scifihifi-iphone code for keychain and save password using
[SFHFKeychainUtils storeUsername:#"User" andPassword:#"123"
forServiceName:#"TestService" updateExisting:YES error:&error];
When I delete the application from the device, the password remains in the keychain.
I want to remove the password from the keychain when the user deletes the application from the device. How can I do this?
You can take advantage of the fact that NSUserDefaults are cleared by uninstallation of an app. For example:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//Clear keychain on first run in case of reinstallation
if (![[NSUserDefaults standardUserDefaults] objectForKey:#"FirstRun"]) {
// Delete values from keychain here
[[NSUserDefaults standardUserDefaults] setValue:#"1strun" forKey:#"FirstRun"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
//...Other stuff that usually happens in didFinishLaunching
}
This checks for and sets a "FirstRun" key/value in NSUserDefaults on the first run of your app if it's not already set. There's a comment where you should put code to delete values from the keychain. Synchronize can be called to make sure the "FirstRun" key/value is immediately persisted in case the user kills the app manually before the system persists it.
For users looking for a Swift 3.0 version of #amro's answer:
let userDefaults = UserDefaults.standard
if !userDefaults.bool(forKey: "hasRunBefore") {
// Remove Keychain items here
// Update the flag indicator
userDefaults.set(true, forKey: "hasRunBefore")
}
*note that synchronize() function is deprecated
There is no trigger to perform code when the app is deleted from the device. Access to the keychain is dependant on the provisioning profile that is used to sign the application. Therefore no other applications would be able to access this information in the keychain.
It does not help with you aim to remove the password in the keychain when the user deletes application from the device but it should give you some comfort that the password is not accessible (only from a re-install of the original application).
For those looking for a Swift version of #amro's answer:
let userDefaults = NSUserDefaults.standardUserDefaults()
if userDefaults.boolForKey("hasRunBefore") == false {
// remove keychain items here
// update the flag indicator
userDefaults.setBool(true, forKey: "hasRunBefore")
userDefaults.synchronize() // forces the app to update the NSUserDefaults
return
}
C# Xamarin version
const string FIRST_RUN = "hasRunBefore";
var userDefaults = NSUserDefaults.StandardUserDefaults;
if (!userDefaults.BoolForKey(FIRST_RUN))
{
//TODO: remove keychain items
userDefaults.SetBool(true, FIRST_RUN);
userDefaults.Synchronize();
}
... and to clear records from the keychain (TODO comment above)
var securityRecords = new[] { SecKind.GenericPassword,
SecKind.Certificate,
SecKind.Identity,
SecKind.InternetPassword,
SecKind.Key
};
foreach (var recordKind in securityRecords)
{
SecRecord query = new SecRecord(recordKind);
SecKeyChain.Remove(query);
}
Files will be deleted from your app's document directory when the user uninstalls the app. Knowing this, all you have to do is check whether a file exists as the first thing that happens in application:didFinishLaunchingWithOptions:. Afterwards, unconditionally create the file (even if it's just a dummy file).
If the file did not exist at time of check, you know this is the first run since the latest install. If you need to know later in the app, save the boolean result to your app delegate member.
#amro's answer translated to Swift 4.0:
if UserDefaults.standard.object(forKey: "FirstInstall") == nil {
UserDefaults.standard.set(false, forKey: "FirstInstall")
UserDefaults.standard.synchronize()
}
This seems to be the default behavior on iOS 10.3 based on behavior people have been witnessing in beta #2. Haven't found any official documentation about this yet so please comment if you have.
Just add an app setting bundle and implement a toggle to reset the keychain on app restart or something based on the value selected through settings (available through userDefaults)

Resources