I'm using NSUserDefaults in my iOS app to record some specific info about the user's receipt state.
I'd like to confirm:
If the user quits the app, will those defaults remain?
Are they global, for example I'm currently using the following line to either get or set them and across different methods. I just want to be certain the data within it persists - so if I set in method1 then later method2 I use the same line to get, it will have whatever I set in method1:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
If the user quits the app, will those defaults remain?
Yes, they are persistent.
Are they global?
Global in the sense of your whole app: Yes.
Global in the sense of across apps: No. They are in the app's sandbox.
From the NSUserDefaults documentation (see https://developer.apple.com/documentation/foundation/nsuserdefaults?language=objc)
With the exception of managed devices in educational institutions, a user’s defaults are stored locally on a single device, and persisted for backup and restore. To synchronize preferences and other data across a user’s connected devices, use NSUbiquitousKeyValueStore instead.
So for your first question the answer is yes.
Accordind to your second question, doc says:
At runtime, you use NSUserDefaults objects to read the defaults that your app uses from a user’s defaults database. NSUserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value. When you set a default value, it’s changed synchronously within your process, and asynchronously to persistent storage and other processes.
And even so, it declares that the class is thread safe, so you can be sure about persistent results (for your second answer).
Additionally with #Nikolai Ruhe answer.
if I set in method1 then later method2 I use the same line to get, it will have whatever I set in method1: NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
The UserDefaults class is thread-safe.
Two further points.
The uselessness of synchronize has grown up gradually through versions of iOS. It used to be essential; then advisable; then unnecessary. I can’t give you an exact timeline but a lot of it depends on how far back in iOS you want your app to work.
If you try to test some of these things in Xcode, beware. Up to and including iOS 11, synchronize does not write all the way to disk, but only puts data in a “lazy write” queue. Pressing the Stop button in Xcode (or pressing Run and allowing Xcode to stop the previous running app automatically) shuts everything down abruptly, more abruptly than anything a user can do, and the lazy writes are not written out, but lost. This is confusing while you are resting!! I filed a report on it and was told by Apple that (as Microfot would put it) “This behaviour is by design”.
But just to be clear: that second point does not present a problem for real apps in a real environment, only when you are trying to test “save and restart” scenarios. Waiting 5-10 seconds seems to be long enough for the flushed data to make it all the way to disk.
Related
I've implemented an iPhone application that has around 50k users. Switching from iOS7 to iOS8 a lot of these users have experienced a terrible feeling when they thought that they data get lost.
I've implemented the first-import behaviour that I thought was the one suggested by Apple
1) Users launch the App
2) iCloud, automatically, starts synching data previously stored on iCloud
3) At some point user get notified that data from iCloud is ready thanks to NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted
The problem is with 3) At some point:
Users that have to sync a lot of data need minutes to get the synch completed and in the meanwhile they think that their data is lost.
I really don't know how to let my users know that they have to wait to see their data synched, because I don't know when this operation starts.
I'm thinking about a possible solution:
During the first launch of the App, asking to the user if he wants to use iCloud. If he chooses to use it, building the database with iCloud options, so I know exactly that the synch is starting here (I suppose...)
I'm really not sure about how to implement this behaviour since I've always seen Core Data settings into the AppDelegate but to achieve this behaviour I suppose I need to move all the CoreData settings in a Controller.
What do you think about this solution? how are you working around this problem in you Apps?
Your idea is right, at least it is that what we do. But leave it in the appDelegate.
Differentiate between with iCloud and without iCloud when doing the "addPersistentStoreWithType". If you do it with iCloud options, it will directly start to build the local store which is a kind of a placeholder ( I'm sure you know that, but just to make my thoughts clear). As soon as this is done, the sync starts from iCloud. So this is the starting point I understood you were looking for.
You can watch that process using the notifications by NSPersistentStoreCoordinatorStoresDidChangeNotification and inform you user accordingly triggered by that notification.
If you look at "Reacting to iCloud Events" in the docs https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/UsingCoreDataWithiCloudPG/UsingSQLiteStoragewithiCloud/UsingSQLiteStoragewithiCloud.html#//apple_ref/doc/uid/TP40013491-CH3-SW5 there is a detailed desc.
To summarize, the event you're describing is part of the account transitions process. An account transition occurs when one of the following four events is triggered:
Initial import
the iCloud account used did change
iCloud is disabled
your application's data is deleted
During this event, Core Data will post the NSPersistentStoreCoordinatorStoresWillChangeNotification and NSPersistentStoreCoordinatorStoresDidChangeNotification notifications to let you know that an account transition is happening. The transition type we're interested in is NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted.
For information, I've moved all Core Data related code to my own Manager for simplicity and use it with a singleton design pattern. While setting up the singleton, I register the Manager for all relevant notifications (NSPersistentStoreDidImportUbiquitousContentChangesNotification, NSPersistentStoreCoordinatorStoresWillChangeNotification, NSPersistentStoreCoordinatorStoresDidChangeNotification, NSPersistentStoreCoordinatorWillRemoveStoreNotification).
I store several informations in my settings (NSUserDefaults or anything) like the last iCloud state (enabled, disabled, unknown), if the initial import is done or not, etc.
What I end up doing was having a prompt (UIAlertController or anything) to get a confirmation if the user wants to use iCloud or not. I have a displayICloudDialogAndForce:completion: method to do that and only do that if my iCloud state setting is unknown or I use the force parameter.
Then, after the user input, I call a setupCoreDataWithICloud: method, the iCloud boolean parameter depending on the user choice. I would then setup my Core Data stack, on the cloud or not according to the iCloud parameter.
If I'm setting up using iCloud, I would check my settings for the value of an iCloud imported key (boolean). If the value is NO, then I'm presenting a new modal to warn the user about the incoming import that could take some time.
I've registered my manager for different notifications and specially NSPersistentStoreCoordinatorStoresDidChangeNotification. In my storeDidChange: callback, I'm checking the transition type and if it's NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted, I'm changing the content of my modal to show the user that the import was successful and removing it a few seconds later, saving in my settings that the initial import is done.
- (void)storeDidChange:(NSNotification *)notification
{
NSPersistentStoreUbiquitousTransitionType transitionType = [notification.userInfo[NSPersistentStoreUbiquitousTransitionTypeKey] integerValue];
if (transitionType == NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted) {
[settings setDefaults:#(YES) forKey:kSettingsICloudImportedKey];
[ICloudModal dismissWithSuccess];
// ...
}
// Do other relevant things...
}
I use [NSUserDefaults standardDefaults] to store a boolean to see if it is the first time that application is being launched ... If so, the app should show a registration window.
It was working fine till last week, but now, sometimes when I switch to other apps and come back after a little while, I see that registration page loads while it shouldn't.
I used NSLog to see what is stored in [NSUserDefaults standardDefaults] and I see that the values I stored has been set to nil (null) while I haven't done that anywhere in my code.
Does anyone know why the values do reset ?
P.S: Actually the values are not permanently lost , because if I don't do anything in registration page and quit the app instead, It will launch normally the next time I enter the app !!!
A long time ago I encountered this issue, turns out a third party library that I was using uses the same key when storing values to NSUserDefaults. Try searching your project for this key, maybe something else is resetting it.
Here are the ways I know about to lose values in NSUserDefaults, in order of likelihood:
The key was never saved in the first place
Your app deletes it later on
The app is deleted and reinstalled
Another app overwrites or removes the key
The phone or simulator is reset
During the night, the phone is replaced by an identical-looking, different phone
It sounds like, from the discussion here, that you've ruled out 1,2,4, and probably 3 & 5. The only next debug step I can think of is to store the test phone in a locked drawer at all times.
But I'd leave my money on an intermittent problem causing #1. For that, we'd need posted code to investigate.
EDIT -
A high % of NSUserDefaults problems posted here are about storing BOOLs and other scalar types. It looks like the OP knows about wrapping in NSNumbers, but BOOLS in particular are fraught because it's easy to confuse false-y values like NO no and nil, and NSNull instance.
Let's throw that on the list for this question at #2.5. There again, would need code to confirm.
If this is happening while testing, it's normal. The fact that the program is even making this decision (should I show the registration page?) suggests that the app has been forcibly quit and is starting from scratch. When testing, this can result in clearing out the app sandbox as the app is reloaded from Xcode. In the real life of a real user, however, that won't happen (unless the user deletes the app from the device).
Make sure you are calling [[NSUserDefaults standardUserDefaults] synchronize] just after setting preferences and you are not overwriting you preferences.
For a project I'm working on, I would like to save a few bytes of data to the users iPhone between launches of the application. I would like to do this so I can save some state and a few important numbers when the user terminates the app. I have thought about it, and the best place to do this seems like the AppDelegate.
- (void)applicationWillTerminate:(UIApplication *)application
{
// Called when the application is about to terminate.
// Save data if appropriate. See also applicationDidEnterBackground:.
/* this is where I want to begin the process of saving application data */
}
I have heard of writing objects (only a select few allowed such as NSString, NSDictionary, NSArray) to file, but can this be kept around between launches of the app, or does the data go away when the user terminates the app?
Question:
Is there a definitive way to write data to the users iPhone that will stick around after the application quits?
The most common approach is to persist whatever data or state you need when your app enters the background. Then the data will be there if the app is killed while in the background and the app is restarted.
How you store the data really depends on what the data is. Writing the data to a file in the app's sandbox is quite common. Small bits of data can also be stored in NSUserDefaults.
I would like the determine if I have access to files that have been saved with the attribute NSFileProtectionKey = NSFileProtectionCompleteUntilFirstUserAuthentication.
I have tried [UIApplication sharedApplication].protectedDataAvailable, however from my tests it will return NO whenever the device is locked (if a pin code is set) even if the user has unlocked the device at least once since last starting the phone.
It says quite clearly in the docs: The value of this property is NO if data protection is enabled and the device is currently locked, and in this case files that were assigned the NSFileProtectionComplete or NSFileProtectionCompleteUnlessOpen protection key cannot be read or written by your app. i.e. this is not the correct property.
You need use one of the multi-tasking keys that causes the launch the app on boot - e.g. the voip key for UIBackgroundModes.
Mind you, you're pretty much testing the OS at that point - if you set the keys appropriately when creating the files it should work as advertised. If it doesn't then log a radar.
When I'm setting up the default preferences for my app, I'm doing the following:
1) Reading Root.plist from inside Settings.bundle into a dictionary.
2) I test if a preference is set, and if not I'm registering my defaults dictionary via [NSUserDefaults standardUserDefaults] registerDefaults:]
Problem is, defaults registered with registerDefaults: do not go to the persistent store, so if the user never changes their preferences I am reading my default preferences and registering them with NSUserDefaults every time the app launches.
Instead of using registerDefaults: I could use setValue:forKey: and have my default setting go to the persistent store, bypassing the need to build & register a dictionary on each launch. However, Apple's documentation and sample code both point to registerDefaults:.
So I'm trying to figure out when and why I should use registerDefaults: and when/why I should use setValue:forKey: instead?
Problem is, defaults registered with registerDefaults: do not go to the persistent store, so if the user never changes their preferences I am reading my default preferences and registering them with NSUserDefaults every time the app launches.
Yes. Why is that a problem? Why write to disk things you have in the code already?
Instead of using registerDefaults: I could use setValue:forKey: and have my default setting go to the persistent store, bypassing the need to build & register a dictionary on each launch. However, Apple's documentation and sample code both point to registerDefaults:.
Only if you first checked "is this value set? No? OK, now set it." That's more code and cost than just using registerDefaults:.
You should use registerDefaults: to set the defaults. You should use setValue:forKey: to save values that are actively set.
Remember also that NSUserDefaults exists on Mac as well. There, the user can directly access the settings with the defaults command, so the program is not the only entity that can modify this store.