Apple documentation says we need to register for NSUbiquityIdentityDidChangeNotification and to compare the current iCloud token with the one previously stored in NSUserDefaults to detect if a user disabled iCloud from Documents & Data Settings or switched to another iCloud account.
I use a standard UIManagedDocument and I target iOS 7 so the fallback store is handled automatically by CoreData.
I do not understand what I should do after I find the user enabled / disabled iCloud or switched to another account. Should I migrate the persistent store? Or should I migrate it after an NSPersistentStoreCoordinatorStoresDidChangeNotification? Or should I never migrate it because everything is handled by CoreData?
After watching WWDC 2013 207 video several times I thought this would have been handled automatically by Core Data but I found that if I start with iCloud support and then I switch it off from Document & Data Settings and I insert new
data, then I switch back iCloud to on, I end with two different data sets.
I would like that if I find the user disabled iCloud then the local db should contain up to the last change made until the iCloud was enabled and only from this point everything should stop syncing until iCloud will be enabled again.
In the WWDC 2013 207 video, in the Melissa demo, I also noticed a call to a method [self migrateBack] after an NSPersistentStoreCoordinatorStoresDidChangeNotification and this confuses me because the slides just show we should save our context here and refresh the UI, they do not show we should migrate anything:
**Account Changes Now**
NSPersistentStoreCoordinatorStoresWillChangeNotification
[NSManagedObjectContext save:]
[NSManagedObjectContext reset:]
NSPersistentStoreCoordinatorStoresDidChangeNotification
[NSManagedObjectContext save:]
The NSPersistentStoreCoordinatorStoresDidChangeNotification notification has nothing to do with changing the iCloud access.
If the user turns off iCloud access or logs out of iCloud then Core Data has to use the fallback store, which is probably empty! You won't get a chance to migrate anything.
However if the app has its own Use iCloud setting then you can test for that change and if set to NO migrate any documents to local storage assuming iCloud is still available.
You can see the expected behaviour in the latest version of Pages. If you have iCloud documents then as soon as you turn off iCloud Documents & Data in the Settings App you loose all access to the Pages documents. However if you go the Pages settings in the Settings App and turn off Use iCloud and then switch back to Pages you will be prompted to Keep on My iPhone, Delete from My iPhone or Keep using iCloud. That is how Apple will expect your app to work.
When the application enters the foreground we check the app specific iCloud settings and if they have changed we take the necessary action.
If no change has been made then we keep running
If the iCloud setting has changed then:
If it is turned OFF ask the user what to do
If it has been turned ON then share all documents in iCloud
/*! The app is about to enter foreground so use this opportunity to check if the user has changed any
settings. They may have changed the iCloud account, logged into or out of iCloud, set Documents & Data to off (same effect as
if they logged out of iCloud) or they may have changed the app specific settings.
If the settings have been changed then check if iCloud is being turned off and ask the user if they want to save the files locally.
Otherwise just copy the files to iCloud (don't ask the user again, they've just turned iCloud on, so they obviously mean it!)
#param application The application
*/
- (void)applicationWillEnterForeground:(UIApplication *)application
{
//LOG(#"applicationWillEnterForeground called");
// Check if the app settings have been changed in the Settings Bundle (we use a Settings Bundle which
// shows settings in the Devices Settings app, along with all the other device settings).
[[NSUserDefaults standardUserDefaults] synchronize];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
bool userICloudChoice = [userDefaults boolForKey:_cloudPreferenceKey];
// Now compare it with the current apps in memory setting to see if it has changed
if (userICloudChoice == useICloudStorage) {
// No change so do nothing
//LOG(#" iCloud choice has not changed");
} else {
// Setting has been changed so take action
//LOG(#" iCloud choice has been changed!!");
// iCloud option has been turned off
if (!userICloudChoice) {
//LOG(#" Ask user if they want to keep iCloud files locally ?");
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
_cloudChangedAlert = [[UIAlertView alloc] initWithTitle:#"You're not using iCloud" message:#"What would you like to do with documents currently on this phone?" delegate:self cancelButtonTitle:#"Keep using iCloud" otherButtonTitles:#"Keep on My iPhone", #"Delete from My iPhone", nil];
} else {
_cloudChangedAlert = [[UIAlertView alloc] initWithTitle:#"You're not using iCloud" message:#"What would you like to do with documents currently on this phone?" delegate:self cancelButtonTitle:#"Keep using iCloud" otherButtonTitles:#"Keep on My iPad", #"Delete from My iPad", nil];
}
[_cloudChangedAlert show];
// Handle the users response in the alert callback
} else {
// iCloud is turned on so just copy them across... including the one we may have open
//LOG(#" iCloud turned on so copy any created files across");
[[CloudManager sharedManager] setIsCloudEnabled:YES]; // This does all the work for us
useICloudStorage = YES;
}
}
}
- (void)alertView:(UIAlertView*)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (alertView == _cloudChoiceAlert)
{
//LOG(#" _cloudChoiceAlert being processed");
if (buttonIndex == 1) {
//LOG(#" user selected iCloud files");
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:_cloudPreferenceKey];
[[NSUserDefaults standardUserDefaults] setValue:#"YES" forKey:_cloudPreferenceSet];
useICloudStorage = YES;
[[NSUserDefaults standardUserDefaults] synchronize];
[[CloudManager sharedManager] setIsCloudEnabled:YES];
}
else {
//LOG(#" user selected local files");
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:_cloudPreferenceKey];
[[NSUserDefaults standardUserDefaults] setValue:#"YES" forKey:_cloudPreferenceSet];
useICloudStorage = NO;
[[NSUserDefaults standardUserDefaults] synchronize];
[[CloudManager sharedManager] setIsCloudEnabled:NO];
}
}
if (alertView == _cloudChangedAlert)
{ //LOG(#" _cloudChangedAlert being processed");
if (buttonIndex == 0) {
//LOG(#" 'Keep using iCloud' selected");
//LOG(#" turn Use iCloud back ON");
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:_cloudPreferenceKey];
[[NSUserDefaults standardUserDefaults] synchronize];
useICloudStorage = YES;
}
else if (buttonIndex == 1) {
//LOG(#" 'Keep on My iPhone' selected");
//LOG(#" copy to local storage");
useICloudStorage = NO;
[[CloudManager sharedManager] setDeleteICloudFiles:NO];
[[CloudManager sharedManager] setIsCloudEnabled:NO];
}else if (buttonIndex == 2) {
//LOG(#" 'Delete from My iPhone' selected");
//LOG(#" delete copies from iPhone");
useICloudStorage = NO;
[[CloudManager sharedManager] setDeleteICloudFiles:YES];
[[CloudManager sharedManager] setIsCloudEnabled:NO];
}
}
}
/*! Checks to see whether the user has previously selected the iCloud storage option, and if so then check
whether the iCloud identity has changed (i.e. different iCloud account being used or logged out of iCloud).
If the user has previously chosen to use iCloud and we're still signed in, setup the CloudManager
with cloud storage enabled.
If no user choice is recorded, use a UIAlert to fetch the user's preference.
*/
- (void)checkUserICloudPreferenceAndSetupIfNecessary
{
FLOG(#"checkUserICloudPreferenceAndSetupIfNecessary called");
[[CloudManager sharedManager] setFileExtension:_fileExtension andUbiquityID:_ubiquityContainerKey ];
id currentToken = [[NSFileManager defaultManager] ubiquityIdentityToken];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
NSString* userICloudChoiceSet = [userDefaults stringForKey:_cloudPreferenceSet];
bool userICloudChoice = [userDefaults boolForKey:_cloudPreferenceKey];
userICloudChoice = [userDefaults boolForKey:_cloudPreferenceKey];
//FLOG(#" User preference for %# is %#", _cloudPreferenceKey, (userICloudChoice ? #"YES" : #"NO"));
if (userICloudChoice) {
//LOG(#" User selected iCloud");
useICloudStorage = YES;
[self checkUbiquitousTokenFromPreviousLaunch:currentToken];
} else {
//LOG(#" User disabled iCloud");
useICloudStorage = NO;
}
// iCloud is active
if (currentToken) {
//LOG(#" iCloud is active");
// If user has not yet set preference the prompt for them to select a preference
if ([userICloudChoiceSet length] == 0) {
_cloudChoiceAlert = [[UIAlertView alloc] initWithTitle:#"Choose Storage Option" message:#"Should documents be stored in iCloud or on just this device?" delegate:self cancelButtonTitle:#"Local only" otherButtonTitles:#"iCloud", nil];
[_cloudChoiceAlert show];
}
else if (userICloudChoice ) {
[[CloudManager sharedManager] setIsCloudEnabled:YES];
}
}
else {
//LOG(#" iCloud is not active");
[[CloudManager sharedManager] setIsCloudEnabled:NO];
useICloudStorage = NO;
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:_cloudPreferenceKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// Since the user is signed out of iCloud, reset the preference to not use iCloud, so if they sign in again we will prompt them to move data
[userDefaults removeObjectForKey:_cloudPreferenceSet];
}
[self storeCurrentUbiquityToken:currentToken];
}
Related
This question already has answers here:
iOS unique user identifier [duplicate]
(7 answers)
Closed 5 years ago.
Is there a way to identify a device even after having uninstalled an app and reinstalling again? I found topics where it's possible to get a UUID but it seems that after uninstalling the app the value of the UUID changes
The value in this property remains the same while the app (or another
app from the same vendor) is installed on the iOS device. The value
changes when the user deletes all of that vendor’s apps from the
device and subsequently reinstalls one or more of them.
I installed an App called Jodel, you don't have to create an Account to use the app and After uninstalling it, delete iCloud data, logging out from iCloud... an reinstalling it I was still logged in in the App. I assume they use a unique device identifier? Do you have an idea how such mechanism could be implemented?
You can use Keychain Service to store data still after uninstalling app from device.
for more reference about keychain service check this
https://developer.apple.com/documentation/security/keychain_services
Yes, It's Possible
#import "UICKeyChainStore.h"
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self getDeviceIdFromKeychain];
}
- (void)getDeviceIdFromKeychain
{
NSString *deviceID = [UICKeyChainStore stringForKey:#"KEY TO SAVE TO Keychain" service:nil];
// if vandorid from keychain is not nil
if (deviceID)
{
[[NSUserDefaults standardUserDefaults] setObject:deviceID forKey:#"deviceID"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
// else it goes for new vendorid and then stored it to keychan
else if (deviceID == (id)[NSNull null] || deviceID.length == 0 )
{
deviceID = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
[UICKeyChainStore setString:deviceID forKey:#"KEY TO SAVE TO Keychain" service:nil];
[[NSUserDefaults standardUserDefaults] setObject:deviceID forKey:#"deviceID"];
[[NSUserDefaults standardUserDefaults] synchronize];
// NSLog(#"VendorID Local - %#",deviceID);
}
}
ViewContoller.m
- (void)viewDidLoad
{
NSString *strDeviceId = [[NSUserDefaults standardUserDefaults]objectForKey:#"deviceID"];
}
I am using Dropbox and Google Drive integration in my iOS app. I can fetch files from both drives and view listing in tableview. However, when I delete the app on my iPhone without logout from these drives, it still shows logged in when I install new app. How to logout user when I delete the app or remove session?
For Dropbox i am using ObjectiveDropboxOfficial apiV2 and for Google Drive i am using GoogleAPIClientForREST, GTMSessionFetcher etc libraries.
My code:
[DBClientsManager setupWithAppKey:#"my-key"];
[DBClientsManager authorizeFromController:[UIApplication sharedApplication]
controller:self openURL:^(NSURL *url) {
[[UIApplication sharedApplication] openURL:url];
}];
//AppDelegate
if ([DBClientsManager handleRedirectURL:url])
{
if (DBClientsManager.authorizedClient || DBClientsManager.authorizedTeamClient) {
// NSLog(#"App linked successfully!");
// At this point you can start making API calls
NSNotification *notification = [NSNotification notificationWithName:#"DropboxLoggedIn" object:nil];
[[NSNotificationCenter defaultCenter] postNotification:notification];
}
return YES;
}
If these services are designed this way I assume they save credentials in keychain which persists data and your application is already logged in when reinstalled or keychain is anyhow transfered.
If this is not your desired effect I can only assume you will need to log out from these services manually. This means you will need to track these logins and logouts and then when the app starts simply log out from all services which have not been tracked as logged in by you.
It is an ugly thing to do but it is a solution:
When a service is logged in save a value in user defaults
- (void)serviceDidLogin:(ServiceType)type {
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:[self keyForServiceType: type]];
}
Then when it is logged out you need to clear it
- (void)serviceDidLogout:(ServiceType)type {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:[self keyForServiceType: type]];
}
Then when app starts you need to log out from all of the services that you have no recording of being logged into:
- (void)logOutFromAllUnusedService {
for(int i=0; i<ServiceTypeCount; i++) {
ServiceType type = i;
if([[NSUserDefaults standardUserDefaults] boolForKey:[self keyForServiceType: type]] == NO) {
[self logoutFromService:type];
}
}
}
No matter how you do this but my situation assumes ServiceType is an enum like so:
typedef enum {
// Never assign values to enums
ServiceTypeDropbox,
ServiceTypeGoogleDrive,
ServiceTypeCount, // Always last of the valid
// Move deprecated services here
ServiceTypeDeprecated1 // TODO: remove at some point
} ServiceType;
maybe someone could assist me.
When my app is launched for the first time a UIAlertController message appears and asks the user if they Need to GoTo the Settings App. using the following code.
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:#"prefs:root=Wi-Fi"]];
});
Upon return to the App the same UIAlertController Message appears, using this code in the ViewDidLoad:
-(void)viewDidLoad {
if (launched == NO) {
launched = YES;
defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:launched forKey:#"boolKey"];
[defaults synchronize];
My code for the UIAlertController.
} else if (launched == YES) {
[self doSomeThing];
}
}
It appears my Bool Value is not being saved when the settings in the info.plist Application does not run in background: is set to YES, but if the Application does not run in background: is set to NO the else statement is executed. However this is not good because my app is suspended and when launched again I need the original message to appear and it does not, the app is restored to its last state.
Any Suggestions is greatly appreciated.
JZ
1.- Saving into NSUserDefaults does not save anything to the info.plist. NSUserDefaults has its own plist that gets wiped out if you remove your application.
If you want to prevent to launch the same alertview:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if([defaults boolForKey:#"boolKey"]) {
defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:#"boolKey"];
My code for the UIAlertController.
}
else {
//Whatever you need to do if its not first launch
}
Now next time you hit your viewDidLoad, since the boolForKey:#"boolKey" has a YES, you won't hit that code and the alertView won't get presented.
I have an app which shows the settings page ONCE! This is when it is first download from the app store. After that then it goes to the main page only.
but when you swipe the app when you double click the iPhone button and remove the app then it goes back to the settings page.
Here is some code from my app
didfinishwithOptions
if (![[NSUserDefaults standardUserDefaults] boolForKey:#"SettingsShown"])
{
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"SettingsShown"];
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:#"ShowBackButton"];
[[NSUserDefaults standardUserDefaults] synchronize];
self.window.rootViewController = [self.window.rootViewController.storyboard instantiateViewControllerWithIdentifier:#"SettingsViewController"];
}
if ([[NSUserDefaults standardUserDefaults] boolForKey:#"SettingsShown"] == NO)
{
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:#"SettingsShown"];
}
then I have put
- (void)applicationWillTerminate:(UIApplication *)application
{
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:#"SettingsShown"];
NSLog(#"Application is terminated");
}
this should run when you swipe the application when you double click on the iPhone button. but it isn't setting the user default to 0 because it is running the settings page again from didfinishwithoptions. Can anyone advise ?
Any value put into NSUserDefaults is not necessarily written instantly. If you app, for instance, terminates in some unusual way, there is no guarantee, the data will be written.
You can force the system to write to the NSUserDefaults, using synchronize:
[[NSUserDefaults standardUserDefaults] synchronize]
From the documentation:
Writes any modifications to the persistent domains to disk and updates all unmodified persistent domains to what is on disk.
Because this method is automatically invoked at periodic intervals, use this method only if you cannot wait for the automatic synchronization (for example, if your application is about to exit) or if you want to update the user defaults to what is on disk even though you have not made any changes.
There is a minor performance penalty doing so.
I'm implementing an in-app purchase feature. In addition to Apple Store standard purchase flow, I have another receipt validation from my own server side. Sometimes, the purchase procedure is complete on Apple side and the app exist before my server validates the receipt.
So I store the receipt in NSUserDefaults. And whenever applicationDidBecomeActive, I would check if there's pending receipt in NSUserDefaults. If yes, I would like to pop up an alert, asking user whether to continue completing the purchase. If user canceled, then I would remove the receipt cancel the purchase. Otherwise, I would direct user to a purchase view, and do the rest of work.
Previously, I did the checking inside AppDelegate ApplicationDidBecomeActive. It seems not to be a good practice. I then try to move the code into MainViewController, catch AppicationDidBecomeActive notification in init. But I'm not sure what's the correct way of doing so? I try to catch the event with selector:#selector(resumePurchase:) and here's my resumePurchase code
- (void)resumePurchase:(NSNotification*)notification {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSData *purchaseDocument = (NSData*)[userDefaults objectForKey:#"lastPurchaseDocument"];
if (!purchaseDocument) {
NSString *message = [NSString stringWithFormat:NSLocalizedString(#"You have an incomplete purchase in the app, do you want to continue the payment?",#"resume purchase.")];
self.resumePurchaseAlert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(#"Resume Payment",#"alter title")
message:message
delegate:self
cancelButtonTitle:NSLocalizedString(#"Cancel",#"Cancel caption")
otherButtonTitles:NSLocalizedString(#"Continue",#"altert approve button"), nil];
[self.resumePurchaseAlert show];
}
}
This seems ok so far. I'm stuck at the Continue button handler
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (alertView == self.resumePurchaseAlert) {
if (buttonIndex == 0) { // Cancel
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults removeObjectForKey:kPurchaseDocument];
[userDefaults synchronize];
}
if (buttonIndex == 1) { // Continue
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSDictionary *purchaseDocument = (NSDictionary*)[userDefaults objectForKey:kPurchaseDocument];
NSData *purchasedReceipt = [purchaseDocument objectForKey:kPurchasedReceipt];
NSString *purchasedFeature = [purchaseDocument objectForKey:kPurchasedFeature];
if (purchasedFeature && purchasedReceipt) {
// here I want to redirect the view to PurchaseViewController
}
}
}
}
I don't know how can I redirect the view to PurchaseViewController in an elegant way...
in other UIViewController check value that you saved in NSUserDefaults and do nabvigation according to it.