I've encountered a weird bug and would like to check if I'm using my Key value observing of changes to NSUserDefaults correctly.
I have used this code in two places in my app without issues, then I added 3rd controller that observes values for "goldCount" and "energyCount". Now when I set the initial value, the app crashes with exc_bad_access. I'm adding this controller to the view 2 seconds after it's parent view appears using performSelectorAfterDelay.
Just before displaying the game screen, I set these properties:
//crash on this line
[[NSUserDefaults standardUserDefaults] setInteger:200 forKey: goldCount];
[[NSUserDefaults standardUserDefaults] setInteger:150 forKey: energyCount];
Within 3 different view controllers, I have this code in viewDidLoad:
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults addObserver:self
forKeyPath:#"goldCount"
options:NSKeyValueObservingOptionNew
context:NULL];
[defaults addObserver:self
forKeyPath:#"energyCount"
options:NSKeyValueObservingOptionNew
context:NULL];
self.goldLabel.text = [NSString stringWithFormat:#"%i",[[GameDataManager sharedInstance] currentGoldCount]];
self.energyLabel.text = [NSString stringWithFormat:#"%i",[[GameDataManager sharedInstance] currentEnergyCount]];
Here's how the class updates it's labels:
// KVO handler
-(void)observeValueForKeyPath:(NSString *)aKeyPath ofObject:(id)anObject
change:(NSDictionary *)aChange context:(void *)aContext
{
//aKeyPath gives us the name of a user default that has changed
if([aKeyPath isEqualToString:#"goldCount"])
{
//we are interested in the new value
self.goldLabel.text = [NSString stringWithFormat:#"%i",[[aChange objectForKey:#"new"] intValue]];
}else if([aKeyPath isEqualToString:#"energyCount"])
{
self.energyLabel.text = [NSString stringWithFormat:#"%i",[[aChange objectForKey:#"new"] intValue]];
}
}
After adding a call to [[NSUserDefaults standardUserDefaults] synchronize]; I get this exception the second time around:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '(
): An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: goldCount
Observed object:
Change: {
kind = 1;
new = 205;
}
Context: 0x0'
NSUserDefaults is not documented to be KVO compliant so it's not possible to observe defaults by their key. This might be the reason for the crash but without a stack trace it's not possible to tell.
There is a notification you can register for that announces changes to the defaults system: NSUserDefaultsDidChangeNotification.
Related
I am using Notification Extension in my application to change the notification sound.
When the user turns off the UISwitch in the settings page I save in the NSUserDefaults a boolean to keep tracking of the state.
However I have 2 different storyboards for two different languages, each has its own style.
When I am using the storyboard A and I print the state of the boolean in the console, it's printing the state correctly. But when I change the language hence a different storyboard B loads up. The boolean is always returning false.
Although the boolean always return false, I have tested the sound of the notification, it works correctly.
Any idea why the boolean is always returning false? I need the correct value in order to show the state correctly of the ringtone in the settings page.
Here's the code I am using:
- (void)setPlayDefaultSound:(BOOL)playDefaultSound
{
NSUserDefaults *def = [[NSUserDefaults alloc] initWithSuiteName:#"group.com.xx.xxx.NotificationServices"];
[def setBool:playDefaultSound forKey:#"playDefaultSound"];
[def synchronize];
}
- (BOOL)playDefaultSound
{
NSUserDefaults *def = [[NSUserDefaults alloc] initWithSuiteName:#"group.com.xx.xxx.NotificationServices"];
return [def boolForKey:#"playDefaultSound"];
}
And here's a screenshot of the Info.plist of the Notification Services Extension:
You are creating new instances of NSUserDefaults each time. Use the same instance each time.
Use this instead.
- (void)setPlayDefaultSound:(BOOL)playDefaultSound
{
[[NSUserDefaults standardUserDefaults] setBool:playDefaultSound forKey:#"playDefaultSound"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (BOOL)playDefaultSound
{
return [[NSUserDefaults standardUserDefaults] boolForKey:#"playDefaultSound"];
}
There was a mismatch in the name in the capabilities, a typo.
It's working normally now.
Im trying to change count of Push notifications in AppDelegate.
I made a property and synthesise it in AppDelegate.h for store NSDictionary with notifications data.
#property (strong,nonatomic) NSMutableDictionary *pushArray;
When I receive notifications I do it:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
_pusharray = [[userInfo valueForKey:#"aps"] valueForKey:#"badges"];
}
Im getting notification in another file (for example mainViewController) well but I can't remove item in _pusharray.
Im makin it like that:
- (IBAction)touchMaenuButtons:(id)sender {
NSUInteger index = [self.menuButtons indexOfObject:sender];
NSMutableDictionary *pushArray = [(AppDelegate *)[[UIApplication sharedApplication] delegate] pushArray];
NSString *key = [NSString stringWithFormat:#"%lu",(unsigned long)index];
UIButton *button = [_badges objectAtIndex:index];
[button setTitle:#"" forState:UIControlStateNormal];
button.hidden=YES;
[pushArray removeObjectForKey:key];
}
In the end string i receive error:
* Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSDictionaryI removeObjectForKey:]: unrecognized selector sent to instance 0x145a78a0'
Please somebody give me answer how to correct it.
It's not a mutable dictionary
you should use the initialiser below to make the dictionary mutable.
_pusharray = [NSMutableDictionary dictionaryWithDictionary:[[userInfo valueForKey:#"aps"] valueForKey:#"badges"]];
Also as a style thing don't call a dictionary an array
Also personally don't like to see this
[(AppDelegate *)[[UIApplication sharedApplication] delegate] pushArray];
If you need this kind of functionality create your own singleton don't overload the app delegate with this king of thing. The app delegate is for the system to talk to your app you should try and avoid putting your app logic in here if possible.
I'm trying to make my app launch a different view on the first time it is loaded up. I've got this code at the moment which implements that something should happen when the app is first launched. I've got this code but it lacks the code to open Initialviewviewcontroller. I have no idea how to do this so help would be much appreciated
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL hasRunBefore = [defaults boolForKey:#"FirstRun"];
if (!hasRunBefore) {
[defaults setBool:YES forKey:#"FirstRun"];
[defaults synchronize];
// what goes here??
else
{
NSLog (#"Not the first time this controller has been loaded");
So I should launch a different view controller in the if statement. But what should I put ?
Solution No. 1
I've written a simple snippet for this thing because I use it quite a lot. You can find it here.
Feel free to use it, fork it or modify it!
Solution No. 2
You can do something like this in your AppDelelegate.m
Add this simple method at the bottom:
- (BOOL)hasEverBeenLaunched
{
// A boolean which determines if app has eer been launched
BOOL hasBeenLaunched;
// Testig if application has launched before and if it has to show the home-login screen to login
// to social networks (facebook, Twitter)
if ([[NSUserDefaults standardUserDefaults] boolForKey:#"HasAlreadyLaunched"]) {
// Setting variable to YES because app has been launched before
hasBeenLaunched = YES;
// NSLog(#"App has been already launched");
} else {
// Setting variable to NO because app hasn't been launched before
hasBeenLaunched = NO;
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"HasAlreadyLaunched"];
[[NSUserDefaults standardUserDefaults] synchronize];
// NSLog(#"This is the first run ever...");
}
return hasBeenLaunched;
}
After implementation of this method you can use it like that:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Determining Storyboard identifier for first view
NSString *storyboardID = [self hasEverBeenLaunched]? #"MainView" : #"LoginView";
// Setting proper view as a rootViewController
self.window.rootViewController = [self.window.rootViewController.storyboard instantiateViewControllerWithIdentifier:storyboardID];
return YES;
}
I have switches and I want to detect if any of switch was changed position, if changes was made I need to start my action.
Switches stores position in NSUserDefaults
- (IBAction)saveSwitch:(id)sender
{
NSUserDefaults *defs1 = [NSUserDefaults standardUserDefaults];
[defs1 setBool: blackSwitch.on forKey: #"blackKey"];
NSUserDefaults *defs2 = [NSUserDefaults standardUserDefaults];
[defs2 setBool: greenSwitch.on forKey: #"greenKey"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
You can post a notification whenever you call synchronize
[[NSNotificationCenter defaultCenter] postNotificationName:#"MyAppSettingsChanged" object:self userInfo:nil];
Then in your other class listen to the notification.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(onAppSettingsChanged:) name:#"MyAppSettingsChanged" object:nil];
-(void) onAppSettingsChanged:(NSNotification)notification
{
// settings changed
}
If you want, you can pass an NSDictionary into userInfo when calling postNotificationName that contains information like which settings have changed.
If you're using NSUserDefaults the easiest is to subscribe NSUserDefaultsDidChangeNotification. It is automatically sent when something changes.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(appSettingsDidChange:)
name:NSUserDefaultsDidChangeNotification
object:nil];
The best way to track changes to NSUserDefaults is to add an observer using KVO. This way you do not need to perform any custom notification code or track changes manually.
In the class that wants to be informed about the changes just register it as a listener to the specified keys:
[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:#"blackKey" options:NSKeyValueObservingOptionNew context:nil];
[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:#"greenKey" options:NSKeyValueObservingOptionNew context:nil];
Then just respond to the notification:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (object == defaults) {
// Here you can grab the values or just respond to it with an action.
}
}
Now whenever one of those keys changes you will be notified automatically.
This is a super clean solution and allows for some heavy reuse. For example, if you add the NSKeyValueObservingOptionInitial key to the options parameter above (NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) then it will also notify your observer method with the initial value, allowing you to reuse that method even for initial states.
Swift Version
Setting up the defaults:
NSUserDefaults.standardUserDefaults().addObserver(self, forKeyPath: "blackKey", options: .New, context: nil)
NSUserDefaults.standardUserDefaults().addObserver(self, forKeyPath: "greenKey", options: .New, context: nil)
The observer:
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if object is NSUserDefaults {
// Here you can grab the values or just respond to it with an action.
}
}
You cannot detect changes in NSUserDefaults. Instead, track when the switch itself is changed, and handle that event. Example:
[blackSwitch addTarget:self
action:#selector(blackSwitchChanged:)
forControlEvents:UIControlEventValueChanged];
Handle the switch position changing:
- (IBAction)blackSwitchChanged:(id)sender {
NSLog(#"Black switch changed");
..
// check if blackSwitch is on or off.
}
I want to store an array with NSUserDefault, then, I put in applicationDidEnterBackground
[[NSUserDefaults standardUserDefaults] setObject:myArray forKey:#"myArray"];
and in application didFinishLaunchingWithOption
myArray= [[NSMutableArray alloc]
initWithArray:[[NSUserDefaults standardUserDefaults]
objectForKey:#"myArray"]];
it's ok for multitasking device, but for not-multitasking device, how can I solve?
Store the object in NSUserDefaults in -applicationWillTerminate:, if it hasn't already been saved by the invocation of -applicationDidEnterBackground: (i.e. check if multitasking is supported, if it is, then don't save it because it's already been saved.)
- (void) applicationWillTerminate:(UIApplication *) app {
if([[UIDevice currentDevice] respondsToSelector:#selector(isMultitaskingSupported)] &&
![[UIDevice currentDevice] isMultitaskingSupported]) {
[[NSUserDefaults standardUserDefaults] setObject:myArray forKey:#"myArray"];
}
}
Do not forget to sync the buffer before going into background:
[[NSUserDefaults standardUserDefaults] synchronize];
The previous answers are all correct, but note that neither applicationDidEnterBackground nor applicationWillTerminate are guaranteed to be called in all situations. You are usually better off storing important data whenever it has changed.
Save NSUserDefaults at
- (void)applicationWillTerminate:(UIApplication *)application
set
[[NSUserDefaults standardUserDefaults] setObject:myArray forKey:#"myArray"];
in
applicationWillTerminate
and don't forget to use the encodeWithCoder and initWithCoder inside the object that you are trying to save and that is contained in the array