NSUserDefaults setObject:nil vs. setNilValueForKey - ios

My app crashes whenever I try the following line:
[[NSUserDefaults standardUserDefaults] setNilValueForKey:#"my_key"];
with this error: 'NSInvalidArgumentException', reason: '[<NSUserDefaults 0x7a2423d0> setNilValueForKey]: could not set nil as the value for the key my_key.'
But when I do this it seems to work:
[[NSUserDefaults standardUserDefaults] setObject:nil forKey:#"my_key"];
Can someone explain the difference between these 2 functions?

setNilValueForKey: is part of the NSKeyValueCoding protocol and is not intended to be called directly, just overridden by classes with custom NSKeyValueCoding implementations. setObject:forKey:, however, is a method provided by NSUserDefaults and essentially removes the key from the defaults when sent with a nil object (though this behavior may look different in Swift).

According to Apple doc : "The default implementation raises an NSInvalidArgumentException." Ovbiusly when you call it the NSInvalidArgumentException will be launched.
You can call, but you will are launching an exception. Where use Apple this methods, well according to its doc. again: "Invoked by setValue:forKey: when it’s given a nil value for a scalar value (such as an int or float)."
Let's see a little example:
We create a new class, TestObject, we only add a couple of properties in its header (.h) file:
#import <Foundation/Foundation.h>
#interface TestObject : NSObject
#property (nonatomic, strong) NSString *keyString;
#property (nonatomic) int keyInteger;
#end
Well, now we import it in our viewController and we add the next code to the viewDidLoad methods in order to test:
TestObject *testObject = [[TestObject alloc] init];
[testObject setValue:#"Hola" forKey:#"keyString"];
[testObject setValue:#2 forKey:#"keyInteger"];
#try {
// This is the method wich launch the exception.
[testObject setValue:nil forKey:#"keyInteger"];
// If you test this the exception won't be launched.
// [testObject setKeyInteger:nil];
}
#catch (NSException *exception) {
NSLog(#"The exception name is %#",[exception name]);
}
NSLog(#"View values: %#\n%d",testObject.keyString,testObject.keyInteger);
Of course we can override this methods in our subclass, and change its behavior.
All info: https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Protocols/NSKeyValueCoding_Protocol/#//apple_ref/occ/instm/NSObject/setNilValueForKey:

Related

How to update NSKeyedUnarchiver unarchiveObjectWithData to NSKeyedUnarchiver unarchivedObjectOfClass:[fromData:error:

My app currently uses this deprecated function:
id unarchivedObject=[NSKeyedUnarchiver unarchiveObjectWithData:codedData];
if([unarchivedObject isKindOfClass:[NSDictionary class]]){
// currently returns TRUE when reading existing user data.
}
To update, I've converted to this:
id unarchivedObject=[NSKeyedUnarchiver unarchivedObjectOfClass:[NSDictionary class] fromData:codedData error:nil];
if([unarchivedObject isKindOfClass:[NSDictionary class]]){
// currently returns FALSE when reading existing user data.
}
The data was originally encoded like this:
-(void)encodeWithCoder:(NSCoder*)encoder{
[encoder encodeObject:text forKey:#"text"];
}
-(instancetype)initWithCoder:(NSCoder*)decoder{
if(self=[super init]){
text=[decoder decodeObjectForKey:#"text"];
}
What could be causing the IF statement to return FALSE using the newer code?
Please note that I am concerned primarily with reading existing data stored prior to deprecating the Archiving functions. Simply changing to the newer functions does not resolve the issue.
Interesting question! I've been supporting iOS 10.0 so I haven't encountered such issue until I saw this. I was tinkering for an hour and I successfully found the issue.
What could be causing the IF statement to return FALSE using the newer
code?
It's because your unarchivedObject object is nil!
If you use the parameter error in the new method, you would see an error like this:
Error Domain=NSCocoaErrorDomain Code=4864 "This decoder will only
decode classes that adopt NSSecureCoding. Class 'QTPerson' does not
adopt it." UserInfo={NSDebugDescription=This decoder will only decode
classes that adopt NSSecureCoding. Class 'QTPerson' does not adopt it.
But how do we get the correct value for this unarchivedObject and not nil? It would take a couple of steps.
First off, make your model/class conform to <NSCoding, NSSecureCoding>
Example:
QTPerson.h
#import <Foundation/Foundation.h>
#class QTPerson;
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Object interfaces
#interface QTPerson : NSObject <NSCoding, NSSecureCoding>
#property (nonatomic, copy) NSString *text;
#end
NS_ASSUME_NONNULL_END
And then implement the protocol methods:
QTPerson.m
#import "QTPerson.h"
#implementation QTPerson
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:_text forKey:#"text"];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
_text = [coder decodeObjectOfClass:[NSString class] forKey:#"text"];
}
return self;
}
#end
And then when archiving an object, you would want to pass YES to the parameter requiringSecureCoding, like so:
QTPerson *person = [[QTPerson alloc] init];
person.text = #"Glenn";
NSData *codedData1 = [NSKeyedArchiver archivedDataWithRootObject:person requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setValue:codedData1 forKey:#"boom"];
Lastly, when unarchiving, just do what you did correctly, like so:
NSData *codedData = [[NSUserDefaults standardUserDefaults] dataForKey:#"boom"];
NSError *er;
id unarchivedObject=[NSKeyedUnarchiver unarchivedObjectOfClass:[QTPerson class] fromData:codedData error:&er];
if([unarchivedObject isKindOfClass:[QTPerson class]]){
NSLog(#"TRUE!");
} else {
NSLog(#"FALSE!");
}
Voila! You'll get nonnull object unarchivedObject, hence the TRUE/YES value you're looking for!

Set NSObject property - EXC_BAD_ACCESS

Process :
App is on the home view controller and is requesting data on API to set an NSObject property. The request is processing on a private method.
User change the view controller to a second view controller (the request is still processing asynchronously)
The second view controller is loaded
The request is ending and app return EXC_BAD_ACCESS when it setting the object property
It seems the object has not the correct memory access.
I would like than the user can switch view controller, even if there is a request pending, and the application doesn't crash.
I don't want to block the user on the view controller during loading.
User.h
#interface User : NSObject
[...]
#property (nonatomic) NSString *notification;
[...]
-(void) methodName;
#end
User.m
-(void) methodName{
//there is code around but useless for this problem
[...]
NSError *error;
NSMutableDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
self.notification = [[dict objectForKey:#"infos"] objectForKey:#"notification"]; //Here is the EXC_BAD_ACCESS
[...]
}
MyController.m
#interface MyController ()
#end
User *user;
#implementation HomeCVViewController
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:YES];
user = [User new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[user someMethod];
dispatch_async(dispatch_get_main_queue(), ^{
[...]//some code
});
});
}
#end
EDIT :
I just put #property (nonatomic) User *user; in MyController.h and remove User *user; in MyController.m . The user is not deallocated and there is no crash.
Verify that [dict objectForKey:#"infos"] is not NSNull - Crash
can be here.
Other code looks OK.
Also add -(void)deallocto your object and put a
break point there to verify that the object is not being released
before the assignment.
Check your output at the NSError
NSError *error;
NSMutableDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
NSLog(#"%#", error.localizedDescription); <----- HERE
self.notification = [[dict objectForKey:#"infos"] objectForKey:#"notification"];
When you try to convert from/to JSON, improper JSON structure will cause it to crash. See the error for more information.
Usually, this type of error appears when object deallocated but your code try to access it's data
so, first of all, check how do you hold your User entity in memory, throw, for example, property:
#property (nonatomic, strong) User *u;
and make sure it is still in memory when operation completed:
Hope this will help you.
Instead of
self.notification = [[dict objectForKey:#"infos"] objectForKey:#"notification"]; //Here is the EXC_BAD_ACCESS
write
NSDictionary* infoDict = dict [#"infos"];
id notification = infoDict [#"notification"];
self.notification = notification;
set a breakpoint, and examine each of infoDict, notification, and self. That takes the guesswork out of it. Right now you don't even know which of these three assignments goes wrong.
And since you have lots of lines of code that are in your opinion bug free and irrelevant, chances are good that the actual bug is hiding somewhere in those lines. Remember: There is a bug in your code. Assuming that any of your code is bug free is daft, because you already know it isn't!

Unable to access static variables from NSObject class but I am able from UIViewController

Ok, strange thing occurred and I guess answer is quite simple, but I fail to figure out what's going on.
Situation is next:
I have an NSObject class called Constants.
Constants.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <GooglePlus/GooglePlus.h>
#interface Constants : NSObject
+(Constants*)shared;
#property GTLPlusPerson* googlePlusUser;
#property int profileType;
#property NSString *userName, *userLastName, *userEmail, *userGoogleId,*userProfilePicture;
#end
Constants.m
#import "Constants.h"
#implementation Constants
#synthesize profileType, userProfilePicture, userLastName,userName,userGoogleId,userEmail;
static Constants *constants = nil;
+ (Constants*)shared {
if (nil == constants) {
constants = [[Constants alloc] init];
}
return constants;
}
I use this class in order to save some static variables that I will use throughout the app.
Now, If I try and declare one of the variables like
[Constants shared].userName = #"name";
from an NSObject class method (Which I call from a ViewController), I fail to do so.
But If I declare Constant variables directly from ViewController (after viewDidLoad for example) everything works fine.
Here is the Class I try to declare variables from, but I fail (It also has singleton in it, that might be the source of the problem, but im not sure why would it)
#implementation GoogleLogin
static GoogleLogin* gLogin = nil;
+(GoogleLogin*)shared
{
if (nil == gLogin){
gLogin = [[[self class]alloc]init];
}
return gLogin;
}
-(void)getProfile
{
GTLServicePlus* plusService = [[GTLServicePlus alloc] init];
plusService.retryEnabled = YES;
[plusService setAuthorizer:[GPPSignIn sharedInstance].authentication];
GTLQueryPlus *query = [GTLQueryPlus queryForPeopleGetWithUserId:#"me"];
plusService.apiVersion=#"v1";
[plusService executeQuery:query
completionHandler:^(GTLServiceTicket *ticket,
GTLPlusPerson *person,
NSError *error) {
if (error){
NSLog(#"Error while fetching user profile: %#", error);
}else{
NSLog(#"User profile information fetched OK");
[Constants shared].googlePlusUser = person;
[Constants shared].profileType = 1;
[Constants shared].userName = person.name.givenName;
[Constants shared].userLastName = person.name.familyName;
[Constants shared].userEmail = [GPPSignIn sharedInstance].authentication.userEmail;
[Constants shared].userGoogleId = person.identifier;
[Constants shared].userProfilePicture = person.image.url;
NSLog(#"%# %# %# %# %# ",person.name.givenName,person.name.familyName,[GPPSignIn sharedInstance].authentication.userEmail,person.identifier,person.image.url);
}
}];
}
and this is how I call those methods, from my ViewController:
- (IBAction)signupWithGoogle:(UIButton *)sender {
//if i call this method here, on button click, it will finish all the steps needed, except setting constant variables
[[GoogleLogin shared] googleLoginFromViewController:self];
//if I uncomment next line, username will be declared and I will be able to access it later
//[Constants shared].userName = #"Petar";
}
Can anybody figure out why is this happening and what should I do to change that?
When you define a property is strongly suggested to declare the attributes to use with it. I guess the compiler should complain about this with a message like
No 'assign', 'retain', or 'copy' attribute is specified - 'assign' is
assumed
So, use the following instead (copy semantics is fine for mutable classes).
#property (nonatomic, copy) NSString *myString;
You should also specify if the property should be accessed in a atomic or nonatomic way. If you don't specify it, the former will be applied.
Then, you are using a singleton pattern. The suggested way is to use GCD like so.
+ (ConstantsManager*)sharedManager {
static ConstantsManager *sharedManager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[[self class] alloc] init];
});
return sharedManager;
}
Well you did not set your property attributes on the singleton class.
For example,
#property (nonatomic, strong, readonly) ...
Have you tried moving the property assignments out of the completionHandler? It may be that your properties are being assigned on a background thread and your view controller is not catching the assignment. An easy way to check is to override the setters and getters and put breakpoints in them to see what order they are being accessed.
1) Remove the #synthesize because it's not needed (properties will be synthesized as _property automatically)
2) Override setter & getter
-(void)setProfileType:(NSInteger)profileType {
_profileType = profileType;
}
-(NSInteger)profileType {
return _profileType;
}
3) Place breakpoints within these methods and see if the getter is being called before the setter. Alternatively, if simply moving the assignments out of the completionHandler fixes it you know you have some concurrency issues.
I suggest reading up on atomic/nonatomic properties, #synthesize and Objective-C concurrency.

EXC_BAD_ACCESS when passing NSString to method from within a block

I have a method which requests access to Twitter, and then sets the username using - (void) setUsername:(NSString *)username. I have to pass ACAccountStore.requestAccessToAccountsWithType... a completion handler (block) as follows:
[self.accounts requestAccessToAccountsWithType:twitterAccountType options:NULL completion:^(BOOL granted, NSError *actualError) {
if(granted == YES) {
self.haveTwitterAccess = YES;
ACAccount *account = [self.accounts accountsWithAccountType:twitterAccountType][0];
[self setUsername: account.username];
// [...]
When I do this, however, I get EXC_BAD_ACCESS in the setUsername method (to which I'm trying to pass account.username). If I try NSLog(#"%#", account.username); then I can see that the value is correct, but when passed to setUsername, the username variable is nil.
I imagine that it is being released in between the threads somewhere here, but am not sure, what with all this new ARC stuff, how to stop it happening. In essence, this boils down to passing strings between threads I suppose?
I've tried things like [account.username copy] and setting the accounts store as retain etc., but nothing seems to work. I've also tried referencing self using:
__block UserManager *blockSafeSelf = self;
How to I ensure that the variable passed from within a callback block to a method on another object is retained?
EDIT:
This is in a class called UserManager which has a property
#property (nonatomic) ACAccountStore *accounts;
which deals with the Twitter access. It also has the method:
- (void) setUsername: (NSString *) username;
which I'm trying to call with the account username inside the completion block. I hope that helps pinpoint the issue a bit better.
EDIT (2):
username is a property of UserManager and is declared as follows:
#property (nonatomic) NSString *username;

Objective C - Global variables for every View Controller

I'm learning Objective C & Xcode, by doing my first App.
First a user has to sign in. And then he can do several things, like joining a group or changing his data (username, email,...).
The Login is finished and it works fine.
To the question:
Is it possible to set a variable which I can reach from every View Controller?
I tried it with the segues, but I think it's very hard to define this in every View Controller.
I'm searching for a global variable which I can reach from everywhere in the App, is this possible?
Or is there an other method to solve this?
Thank you for the help!
Emanuel
I would personally advise you to use the Singleton pattern.
I would not recommend putting everything into AppDelegate, this class is not meant for that.
Instead, what you can do is create a dedicated class (with a name like "ApplicationState", or whatever suits you), and put the properties you need in its header file, and having your singleton management code in the .m (and the prototype in the header)
If you need the singleton management code, here it is:
+ (ApplicationState*)sharedInstance
{
static ApplicationState* sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
Then, if you have in the header:
#property (nonatomic, strong) NSObject* object;
+ (ApplicationState*)sharedInstance;
You will be able to get this variable from anywhere by including the ApplicationState header file, and call:
[[ApplicationState sharedInstance] object];
You can use NSUserdefault if its simple .
Save:
[[NSUserDefaults standardUserDefaults]setObject:username forKey:#"userName"];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog(#"username saved = %#", username);
Read:
NSString *username = [[NSUserDefaults standardUserDefaults] objectForKey:#"userName"];
NSLog(#"username retrieved = %#", username);
You can use AppDelegate as well.
Define a property in your AppDelegate.h,
#property (nonatomic, strong) NSString *userName;
And then in your view controller, after importing AppDelegate.h,
AppDelegate *appDel = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSLog(#"%#", appDel.userName);
You can put them in plist or you can create Objective-C class initialize your variables within and import header in .pch file, then this class with data will available in every ViewController

Resources