iOS - encoding/decoding enums - crash on accessing after relaunch - ios

What am I doing wrong? Am I encoding/decoding the enum type properly?
GameSettings interface:
typedef enum {
CatWhite = 0,
CatBlack = 1,
CatOrange = 2
} CatColor;
...
CatColor catColor;
...
#property CatColor catColor;
GameSettings implementation:
#synthesize catColor;
...
+ (GameSettings*)GetInstance
{
if (sharedSingleton == nil) {
sharedSingleton = [[super allocWithZone:NULL] init];
sharedSingleton.catColor = CatWhite;
}
return sharedSingleton;
}
-(void)encodeWithCoder:(NSCoder *)coder {
[coder encodeInt:self.catColor forKey:#"CatColor"];
}
-(id)initWithCoder:(NSCoder *)coder {
if((self = [super init])) {
self.catColor = [coder decodeIntForKey:#"CatColor"];
}
NSLog(#"initWithCoder: %d", self.catColor); //this logs the correct int
return self;
}
AppDidFinishLaunching:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [defaults objectForKey:#"GameSettings"];
GameSettings *gGameData = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if(gGameData != NULL) {
GameSettings *settings = [GameSettings GetInstance];
settings.catColor = gGameData.catColor;
}else {
GameSettings *settings = [GameSettings GetInstance];
settings.catColor = CatWhite;
}
...
}
Save settings:
GameSettings* settings = [GameSettings GetInstance];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:settings];
[defaults setObject:data forKey:#"GameSettings"];
[defaults synchronize];
The crash (Program received signal: “EXC_BAD_ACCESS”) comes when I re-launch the app and try to access the game settings:
GameSettings* settings = [GameSettings GetInstance];
NSLog(#"settings: %d", settings); //EXC_BAD_ACCESS
NSLog(#"catColor: %d", settings.catColor); //EXC_BAD_ACCESS
Why can't I access the GameSettings singleton after a re-launch?

Are you sure you want to use applicationDidFinishLaunching there? When you resurrect the program, it may be invoking awakeFromNib instead of applicationDidFinishLaunching (source: http://www.cimgf.com/2008/03/26/cocoa-tutorial-awakefromnib-vs-applicationdidfinishlaunching/ ).

Finally figured this out. GameSettings was getting deallocated because NSKeyedUnarchiver was autoreleasing it:
GameSettings *gGameData = [NSKeyedUnarchiver unarchiveObjectWithData:data];
Fix:
GameSettings *gGameData = [[NSKeyedUnarchiver unarchiveObjectWithData:data] retain];

Related

Objective C - Saving a variable inside NSUserDefaults

I have a class called "EntityType" that has a string "name" and I am using NSUserDefaults to save it so I tried this:
if (button.selected)
{
[button setSelected:YES];
[[NSUserDefaults standardUserDefaults]setObject:[NSNumber numberWithBool:YES] forKey:#"buttonSelected"];
}
I want to access the variable from another class and save it inside NSUserDefaults
You have missed sync call.Apply this to save it.
if (button.selected)
{
[button setSelected:YES];
[[NSUserDefaults standardUserDefaults]setObject:[NSNumber numberWithBool:YES] forKey:#"buttonSelected"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
to get it back later
NSNumber* savedValue = [[NSUserDefaults standardUserDefaults]
objectForKey:#"buttonSelected"];
This is if you want to store only the value.
If you want to store your custom object look at this link
How to store custom objects in NSUserDefaults
You can get NSNumber object like,
NSNumber *num = [[NSUserDefaults standardUserDefaults] objectForKey:#"buttonSelected"];
from anywhere in the app.
On your EntityType class, implement the following two methods for encoding and decoding (something relevant to your own object):
- (void)encodeWithCoder:(NSCoder *)encoder {
//Encode properties, other class variables, etc
[encoder encodeObject:self.name forKey:#"name"];
}
- (id)initWithCoder:(NSCoder *)decoder {
if((self = [super init])) {
//decode properties, other class vars
self.name = [decoder decodeObjectForKey:#"name"];
}
return self;
}
Reading and writing from NSUserDefaults:
- (void)saveCustomObject:(MyObject *)object key:(NSString *)key {
NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:encodedObject forKey:key];
[defaults synchronize];
}
- (MyObject *)loadCustomObjectWithKey:(NSString *)key {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedObject = [defaults objectForKey:key];
MyObject *object = [NSKeyedUnarchiver unarchiveObjectWithData:encodedObject];
return object;
}
Code borrowed from: save class in NSUserDefaults
**ViewController.h**
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
//here i took one userdefault property
#property(strong,nonatomic)NSUserDefaults *user;
#end
**ViewController.m**
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//after the property declaration take standard user defaults
_user = [NSUserDefaults standardUserDefaults];
//here i took Bool and key you can take any thing but took my_val
[_user setBool:YES forKey:#"my_val"];
}
// this is button action
- (IBAction)submit:(id)sender
{
//for bool i am checking the condition given above
if ([_user boolForKey:#"my_val"])
{
**enter code here**
NSLog(#"values is set to yes");
}
else
{
**enter code here**
NSLog(#"Value is set to No");
}
}

Troubles Saving NSString In SpriteKit

I have already figured out how to save NSInteger values in my spritekit game but now that I'm attempting to save an NSString value, my game keeps crashing. The error I get is:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFConstantString string]: unrecognized selector sent to instance 0xe66c58`
My Code:
#import "GameState.h"
#implementation GameState
+ (instancetype)sharedInstance
{
static dispatch_once_t pred = 0;
static GameState *_sharedInstance = nil;
dispatch_once( &pred, ^{
_sharedInstance = [[super alloc] init];
});
return _sharedInstance;
}
- (id) init
{
if (self = [super init]) {
// Init
_score = 0;
_highScore = 0;
_spaceShipUpgrades = 0;
_activeShip = nil;
// Load game state
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
id highScore = [defaults objectForKey:#"highScore"];
if (highScore) {
_highScore = [highScore intValue];
}
id spaceShipUpgrades = [defaults objectForKey:#"spaceShipUpgrades"];
if (spaceShipUpgrades){
_spaceShipUpgrades = [spaceShipUpgrades intValue];
}
id activeShip = [defaults objectForKey:#"activeShip"];
if (activeShip){
_activeShip = [activeShip string];
}
}
return self;
}
- (void) saveState {
// Update highScore if the current score is greater
_highScore = MAX(_score, _highScore);
// Store in user defaults
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:[NSNumber numberWithInt:_spaceShipUpgrades] forKey:#"spaceShipUpgrades"];
[defaults setObject:[NSNumber numberWithInt:_highScore] forKey:#"highScore"];
[defaults setObject:[NSString stringWithString:_activeShip] forKey:#"activeShip"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
#end
The problem is most likely caused by [activeShip string]. If it is a string, it does not respond to the selector string which subsequently crashes your application. As the error says, you are sending string to __NSCFConstantString which has no such method.
If activeShip in this context is always an NSString, simply use it as is and don't send it a string message. You can log an object's class with
NSLog(#"Class of object is %#.", NSStringFromClass([anObject class]));
Or check for class type in general using:
if ([object isKindOfClass:[NSString class]]) {
// ...
}
As general remark I would replace _sharedInstance = [[super alloc] init]; with _sharedInstance = [[GameState alloc] init];.

NSCoding save NSMutableArray on shutdown and load on startup

I am trying to save a NSMutableArray when the app shuts down, and then load it on startup to a tableview.
Here's my code:
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:_savedText forKey:#"savedText"];
}
- (id)initWithCoder:(NSCoder *)decoder {
if((self = [super initWithCoder:decoder])) {
_savedText = [decoder decodeObjectForKey:#"savedText"];
}
return self;
}
I use this code to save the MutableArray:
- (void)applicationDidEnterBackground:(UIApplication *)application {
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:_savedText];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:#"savedText"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
And this to load it:
- (void)viewDidLoad {
NSData *savedData = [[NSUserDefaults standardUserDefaults] objectForKey:#"savedText"];
NSMutableArray *array = [NSKeyedUnarchiver unarchiveObjectWithData:savedData];
array = [_savedText copy];
}
There are no errors, but the table view is empty on startup. Have I forgotten something?
You should read the data from the defaults and set to _savedText object. You are reading the data into var named array and then assigning array = [_savedText copy]. This will probably be nil. It should be the reverse.
- (void)viewDidLoad {
NSData *savedData = [[NSUserDefaults standardUserDefaults] objectForKey:#"savedText"];
NSMutableArray *array = [NSKeyedUnarchiver unarchiveObjectWithData:savedData];
_savedText = [array copy];
}

Working with NSUserDefaults when saving own datatype

i had read this topic How to save My Data Type in NSUserDefault? and get from there this useful part of code:
MyObject *myObject = [[MyObject alloc] init];
NSData *myObjectData = [NSData dataWithBytes:(void *)&myObject length:sizeof(myObject)];
[[NSUserDefaults standardUserDefaults] setObject:myObjectData forKey:#"kMyObjectData"];
for saving data and this for reading
NSData *getData = [[NSData alloc] initWithData:[[NSUserDefaults standardUserDefaults] objectForKey:#"kMyObjectData"]];
MyObject *getObject;
[getData getBytes:&getObject];
its works very good when i save data in one ViewController and read it in other.
but when i whant to use it in the same class:
- (IBAction)linkedInLog:(UIButton *)sender
{
NSUserDefaults *myDefaults = [[NSUserDefaults standardUserDefaults] objectForKey:#"linkedinfo"];
NSData *getData = [[NSData alloc] initWithData:myDefaults];
LinkedContainer *getObject;
[getData getBytes:&getObject];
if (!myDefaults) {
mLogInView = [[linkedInLoginView alloc]initWithNibName:#"linkedInLogInView" bundle:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(loginViewDidFinish:)
name:#"loginViewDidFinish"
object:mLogInView];
[self.navigationController pushViewController:mLogInView animated:YES];
if ((FBSession.activeSession.isOpen)&&(mLinkedInIsLogegOn)) {
mMergeButton.hidden = NO;
}
}
else{
mLinkedInIsLogegOn= YES;
mLinkedInInfo.mConsumer = getObject.mConsumer;
mLinkedInInfo.mToken = getObject.mToken;
}
}
something going wrong. in #selector:loginViewDidFinish i am saving my data to NSUserDefaults:
-(void) loginViewDidFinish:(NSNotification*)notification
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
mLinkedInInfo.mConsumer = mLogInView.consumer;
mLinkedInInfo.mToken = mLogInView.accessToken;
NSData *myObjectData = [NSData dataWithBytes:(void *)&mLinkedInInfo length:sizeof(mLinkedInInfo)];
NSUserDefaults *lSave = [NSUserDefaults standardUserDefaults];
[lSave setObject:myObjectData forKey:#"linkedinfo"];
[lSave synchronize];
if (mLinkedInInfo.mToken) {
mLinkedInIsLogegOn = YES;
}
}
the program always crashes when it comes to else part. If somebody knows what I am doing wrong please help me)
error message: Thread 1 : EXC_BAD_ACCESS(code=2,address 0x8) when compiling getObject.Consumer
In the vast majority of cases, this is not going to be a meaningful way to serialize your object into an NSData:
MyObject *myObject = [[MyObject alloc] init];
NSData *myObjectData = [NSData dataWithBytes:(void *)&myObject length:sizeof(myObject)];
[[NSUserDefaults standardUserDefaults] setObject:myObjectData forKey:#"kMyObjectData"];
The canonical way to do this would be for MyObject to adopt the NSCoding protocol. Based on the code you posted here, an adoption of NSCoding might look like this:
- (id)initWithCoder:(NSCoder *)coder
{
if (self = [super init])
{
mConsumer = [coder decodeObjectForKey: #"consumer"];
mToken = [coder decodeObjectForKey: #"token"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:mConsumer forKey: #"consumer"];
[coder encodeObject:mToken forKey:#"token"];
}
Once you had done that work, you would convert MyObject to and from NSData like this:
NSData* data = [NSKeyedArchiver archivedDataWithRootObject: myObject];
MyObject* myObject = (MyObject*)[NSKeyedUnarchiver unarchiveObjectWithData: data];
The code you have here is totally going to smash the stack and crash (because this line [getData getBytes:&getObject]; will cause the NSData to write bytes to the address of getObject, which is locally declared on the stack. Hence stack smashing.) Starting from your code, a working implementation might look something like this:
- (IBAction)linkedInLog:(UIButton *)sender
{
NSData* dataFromDefaults = [[NSUserDefaults standardUserDefaults] objectForKey:#"linkedinfo"];
LinkedContainer* getObject = (LinkedContainer*)[NSKeyedUnarchiver unarchiveObjectWithData: dataFromDefaults];
if (!dataFromDefaults) {
mLogInView = [[linkedInLoginView alloc]initWithNibName:#"linkedInLogInView" bundle:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(loginViewDidFinish:)
name:#"loginViewDidFinish"
object:mLogInView];
[self.navigationController pushViewController:mLogInView animated:YES];
if ((FBSession.activeSession.isOpen)&&(mLinkedInIsLogegOn)) {
mMergeButton.hidden = NO;
}
}
else{
mLinkedInIsLogegOn= YES;
mLinkedInInfo.mConsumer = getObject.mConsumer;
mLinkedInInfo.mToken = getObject.mToken;
}
}
-(void) loginViewDidFinish:(NSNotification*)notification
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
mLinkedInInfo.mConsumer = mLogInView.consumer;
mLinkedInInfo.mToken = mLogInView.accessToken;
NSData* objectData = [NSKeyedArchiver archivedDataWithRootObject: mLinkedInInfo];
[[NSUserDefaults standardUserDefaults] setObject: objectData forKey: #"linkedinfo"];
[[NSUserDefaults standardUserDefaults] synchronize];
if (mLinkedInInfo.mToken) {
mLinkedInIsLogegOn = YES;
}
}
I agree with ipmcc 's answer, another viable option would be to add methods to your object to convert it to an NSDictionary. You could add methods to -initWithDictionary as well and should make instantiation very easy. Pull from dictionary in NSUserDefaults to use, convert to dictionary to save.
Here is an example of those 2 methods with generic data:
- (id)initWithDictionary:(NSDictionary *)dict
{
self = [super init];
// This check serves to make sure that a non-NSDictionary object
// passed into the model class doesn't break the parsing.
if(self && [dict isKindOfClass:[NSDictionary class]]) {
NSObject *receivedFences = [dict objectForKey:#"fences"];
NSMutableArray *parsedFences = [NSMutableArray array];
if ([receivedFences isKindOfClass:[NSArray class]]) {
for (NSDictionary *item in (NSArray *)receivedFences) {
if ([item isKindOfClass:[NSDictionary class]]) {
[parsedFences addObject:[Fences modelObjectWithDictionary:item]];
}
}
}
}
// More checks for specific objects here
return self;
}
- (NSDictionary *)dictionaryRepresentation
{
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionary];
NSMutableArray *tempArrayForFences = [NSMutableArray array];
for (NSObject *subArrayObject in self.fences) {
if([subArrayObject respondsToSelector:#selector(dictionaryRepresentation)]) {
// This class is a model object
[tempArrayForFences addObject:[subArrayObject performSelector:#selector(dictionaryRepresentation)]];
} else {
// Generic object
[tempArrayForFences addObject:subArrayObject];
}
}
[mutableDict setValue:[NSArray arrayWithArray:tempArrayForFences] forKey:#"fences"];
return [NSDictionary dictionaryWithDictionary:mutableDict];
}
This is basically boilerplate code that is generated by a program I use called JSON Accelerator. It will read a JSON string returned by an API and generate object code for you. Not really a new concept, but makes created classes for API's very easy. And this bit of code works great for creating dictionaries to be saved to NSUserDefaults. Hope this helps.

Object initialized in "init" cannot be accessed in "viewDidLoad"

I have a strange issue: I load object data in "init" method. When I try to access it in the "viewDidLoad" my app crashes. Here is the code:
#interface UploadCenterViewController () {
NSMutableArray *videos;
}
#end
#implementation UploadCenterViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
if (![self loadVideos]) {
[self saveVideos];
}
}
return self;
}
-(void)saveVideos {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedData = [NSKeyedArchiver archivedDataWithRootObject:videos];
[defaults setObject:encodedData forKey:#"VIDEOS"];
[defaults synchronize];
}
-(bool)loadVideos {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedData = [defaults objectForKey:#"VIDEOS"];
if (encodedData) {
videos = (NSMutableArray *)[NSKeyedUnarchiver unarchiveObjectWithData:encodedData];
NSLog(#"array size: %d", [videos count]);
return true;
} else {
videos = [[NSMutableArray alloc] init];
return false;
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"%d", [videos count]);
}
When loading the data, the "videos" array contains an object. When accessing it in the "viewDidLoad" the entire app crashes.
Does anyone has an idea?
Declare videos as an #property and use it as self.videos everywhere. The crash is due to the fact that videos is getting released once you assign a value to it. The scope of videos is only inside that method and it can crash due to this. Since you want to use this outside that method, you need to retain it and you can use #property for this as mentioned below.
for eg:-
#interface UploadCenterViewController () {}
#property(nonatomic, strong) NSMutableArray *videos;
#end
-(void)saveVideos {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedData = [NSKeyedArchiver archivedDataWithRootObject:self.videos];
[defaults setObject:encodedData forKey:#"VIDEOS"];
[defaults synchronize];
}
-(bool)loadVideos {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedData = [defaults objectForKey:#"VIDEOS"];
if (encodedData) {
self.videos = (NSMutableArray *)[NSKeyedUnarchiver unarchiveObjectWithData:encodedData];
NSLog(#"array size: %d", [self.videos count]);
return true;
} else {
self.videos = [[NSMutableArray alloc] init];
return false;
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"%d", [self.videos count]);
}
You might want to retain the result of [NSKeyedUnarchiver unarchiveObjectWithData:encodedData].
Read the memory management rules.

Resources