I have a very strange bug that I've bee trying to track down for several days. I'm saving state data for a game in a file in the app's Documents directory.
Everything had been working fine until a recent iOS update (not sure exactly when, somewhere around 9.0). Suddenly, data is not being archive / unarchived correctly.
The weird part is the code works fine when I run it from Xcode with the iPad tethered to my MAC or when in the emulator. When i download the app from the iTunes using TestFlight, it no longer works. This has made it extremely difficult to debug.
I've checked and double checked everything, I'm using the URL path, added error trapping code, etc. but archiving fails to work correctly when the app is installed from iTunes via TestFlight.
As a last resort I added new code that archives my object, immediately unarchives it into another variable, then display some data in a label. The resulting object contains null data.
No exceptions are thrown.
Just to reiterate, the code doesn't work ONLY when the app in installed from iTunes.
Here is the code snippet;
NSString *documentDirectory = [[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject] path];
NSString* filePath = [documentDirectory stringByAppendingPathComponent:#"playerTest.data"];
LBYPlayerData* pd1 = [[LBYPlayerData alloc ]init];
pd1.currentCountryID = 1;
pd1.lsn = #"123.456";
BOOL success = [NSKeyedArchiver archiveRootObject:pd1 toFile:filePath];
NSAssert(success, #"archiveRootObject failed");
LBYPlayerData* pd2 = nil;
#try {
pd2 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
} #catch (NSException *exception) {
playerDataLabel.text = [NSString stringWithFormat:#"%#",exception.name];
playerUndoDataLabel.text = [NSString stringWithFormat:#"%#",exception.description];
} #finally {
NSAssert((pd2 != nil), #"archivePlayerDataUndo failed to unarchive");
playerDataLabel.text = [NSString stringWithFormat:#"path: %#",filePath];
playerUndoDataLabel.text = [NSString stringWithFormat:#"Undo Country:%li LSN:%#",(long)pd2.currentCountryID,pd2.lsn];
}
Here is the data model
//
// LBYPlayerData.h
#import <Foundation/Foundation.h>
#interface LBYPlayerData : NSObject
#property (nonatomic,readonly) BOOL isNewGame;
#property (nonatomic) NSInteger playerID;
#property (nonatomic) NSInteger usCardIdx;
#property (nonatomic) NSInteger drawDeckIdx;
#property (nonatomic) NSInteger discardDeckIdx;
#property (nonatomic) NSInteger removeDeckIdx;
#property (nonatomic) NSInteger currentCountryID;
#property (nonatomic) NSString* lsn;
#property (nonatomic) NSString* build;
#end
//
// LBYPlayerData.m
#import "LBYPlayerData.h"
#implementation LBYPlayerData
-(id)init
{
self = [super init];
_isNewGame = YES;
return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder
{
// NSLog(#"Saving Player Data");
_isNewGame = NO;
[aCoder encodeBool:_isNewGame forKey: NSStringFromSelector(#selector(isNewGame))];
[aCoder encodeInt64:_playerID forKey: NSStringFromSelector(#selector(playerID))];
[aCoder encodeInt64:_usCardIdx forKey: NSStringFromSelector(#selector(usCardIdx))];
[aCoder encodeInt64:_drawDeckIdx forKey: NSStringFromSelector(#selector(drawDeckIdx))];
[aCoder encodeInt64:_discardDeckIdx forKey: NSStringFromSelector(#selector(discardDeckIdx))];
[aCoder encodeInt64:_removeDeckIdx forKey: NSStringFromSelector(#selector(removeDeckIdx))];
[aCoder encodeInt64:_currentCountryID forKey: NSStringFromSelector(#selector(currentCountryID))];
[aCoder encodeObject:_lsn forKey: NSStringFromSelector(#selector(lsn))];
[aCoder encodeObject:_build forKey: NSStringFromSelector(#selector(build))];
// NSLog(#"Current Counry: %li",(long)_currentCountryID);
}
-(id)initWithCoder:(NSCoder *)aDecoder
{
// NSLog(#"Loading Player Data");
self = [self init];
if (self) {
_isNewGame =[aDecoder decodeBoolForKey:NSStringFromSelector(#selector(isNewGame))];
[self setPlayerID :[aDecoder decodeIntegerForKey:NSStringFromSelector(#selector(playerID))]];
[self setUsCardIdx :[aDecoder decodeIntegerForKey:NSStringFromSelector(#selector(usCardIdx))]];
[self setDrawDeckIdx :[aDecoder decodeIntegerForKey:NSStringFromSelector(#selector(drawDeckIdx))]];
[self setDiscardDeckIdx :[aDecoder decodeIntegerForKey:NSStringFromSelector(#selector(discardDeckIdx))]];
[self setRemoveDeckIdx :[aDecoder decodeIntegerForKey:NSStringFromSelector(#selector(removeDeckIdx))]];
[self setCurrentCountryID:[aDecoder decodeIntegerForKey:NSStringFromSelector(#selector(currentCountryID))]];
[self setLsn :[aDecoder decodeObjectForKey :NSStringFromSelector(#selector(lsn))]];
[self setBuild :[aDecoder decodeObjectForKey :NSStringFromSelector(#selector(build))]];
}
return self;
}
#end
The problem was with NSAssert. I found code that was calling the function to archive the object within an NSAssert statement.
Related
I have a NSMutableaArray of NSString objects. So i'm using NSKeyedArchiever to save it to disk. So when i try to use
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.EventsList forKey:#"Events"];
}
i got an error
Event encodeWithCoder:]: unrecognized selector sent to instance 0x7fd06b542780
Here's my parts of code:
//-------------------Events.h--------------------------
#interface Event : NSObject
#property (strong,nonatomic) NSString *nameOfEvent;
#property (strong,nonatomic) NSString *dateOfEvent;
#property (strong,nonatomic) NSString *placeOfEvent;
#property int priorityOfEvent;
#end
//---------------Singleton.h ----------------
#interface GlobalSingleton : NSObject <NSCoding, NSCopying> {
NSMutableArray *EventsList;
}
#property (nonatomic,retain) NSMutableArray *EventsList;
+(GlobalSingleton *)sharedFavoritesSingleton;
#end
//----------------Singleton.m------------------------
....
#implementation GlobalSingleton
#synthesize EventsList;
....
....
- (void)encodeWithCoder:(NSCoder *)aCoder {
NSLog (#"%#",EventsList); // not nil
[aCoder encodeObject:self.EventsList forKey:#"Events"];
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if ((self = [super init])) {
NSMutableArray *temp = [[NSMutableArray alloc] initWithArray:[aDecoder decodeObjectForKey:#"Events"]];
self.EventsList = temp;
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
GlobalSingleton *copy = [[GlobalSingleton allocWithZone:zone] init];
copy.EventsList = self.EventsList;
return copy;
}
#end
I get textdata from Web-server using ASIFormDataRequest in JSON format, and then i add this object to NSMutableArray, which is also a Singleton, so it looks like this:
NSDictionary *responseDict = [responseString JSONValue];
GlobalSingleton *Singleton = [GlobalSingleton sharedFavoritesSingleton];
for (NSDictionary *str in responseDict) {
Event *newEvent = [[Event alloc] init];
newEvent.nameOfEvent = [str objectForKey:#"EventName"];
newEvent.dateOfEvent = [str objectForKey:#"EventDate"];
newEvent.placeOfEvent = [str objectForKey:#"EventPlace"];
[Singleton.EventsList addObject:newEvent];
}
//------------------Save this data stored in NSMutableArray to disk-------------------------
[NSKeyedArchiver archiveRootObject:Singleton toFile:[self save_path]];
So, again, execution stops on this:
[aCoder encodeObject:self.EventsList forKey:#"Events"];
But when i try to code single NSString object everything goes with no errors.
eventList doesn't contain NSStrings, it contains Event objects.
Your Event class needs to implement encodeWithCoder: - as the exception message says, the Event class doesn't implement this method.
Also you should use a lowercase s for singleton as it is an instance, not a class, and you should probably not use singletons.
I'm making a game using Cocos2d-iphone 2.1, and have encountered an error with NSCoding unarchiving implementation.
I've used the same pattern in my previous projects (made with UIKit), and everything worked perfetly. While when I'm implementing this pattern on cocos2d, it just doesn't work.
I have recreated the error on a sample project (which can be downloaded here https://www.dropbox.com/sh/30cyfczcxeyf8mr/uDcJiNYMdd ) and here's what I'm doing:
I've created a simple singleton class "GameStore".
Then I've created the "GameParameters" class which is a property of the singleton and conforms to NSCoding (therefore can be archived).
In that class there's the "highscore" property of type int (for example) which I'd like to keep archived as a high score value.
In the GameStore class I initialize the GameParameters instance for the first time, if there's an archived object - I unarchive it.
In the "HelloWorldLayer" (which is a default cocos2d placeholder class) I call NSLog to display the "highscore" value and then set it to a new value
In AppDelegate I call the "save data" method to archive data whenever the home button is pressed.
That's it.
When I start the app for the first time, everything works well, and I save the data by pressing the home button.
And when I start the app again, I run into random (as it seems) EXT_BAD_ACCESS errors... Sometimes when I'm getting the highscore value, sometimes when archiving it again.
Any idea what I might be doing wrong here?
Thanks!
GameStore class
Header file:
#import <Foundation/Foundation.h>
#class GameParameters;
#interface GameStore : NSObject
#property (retain, nonatomic) GameParameters * parameters;
+ (GameStore *) game;
- (void) saveData;
#end
Implementation file:
#import "GameStore.h"
#import "GameParameters.h"
static GameStore * game = nil;
#implementation GameStore
#synthesize parameters;
+ (GameStore *) game
{
if (!game) game = [[super allocWithZone:nil] init];
return game;
}
- (id) init
{
if (game != nil) {
return game;
}
self = [super init];
if (self) {
// Initialization
// Try and load the "GameParameters" from archive
parameters = [NSKeyedUnarchiver unarchiveObjectWithFile:[self archivePath]];
NSLog(#"Highscore: %i",parameters.highscore);
// If there's no archive, initialize the "GameParameters"
if (!parameters) {
parameters = [[GameParameters alloc] init];
parameters.highscore = 12345;
}
}
return self;
}
- (void) saveData
{
[NSKeyedArchiver archiveRootObject:parameters toFile:[self archivePath]];
NSLog(#"Data saved");
}
- (NSString *)archivePath
{
NSArray * documentDirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString * documentDir = [documentDirs objectAtIndex:0];
return [documentDir stringByAppendingPathComponent:#"hs.archive"];
}
#end
GameParameters class
Header file:
#import <Foundation/Foundation.h>
#interface GameParameters : NSObject <NSCoding>
#property (nonatomic, assign) int highscore;
#end
Implementation file:
#import "GameParameters.h"
#implementation GameParameters
#synthesize highscore;
- (id) initWithCoder:(NSCoder *)aDecoder
{
highscore = [aDecoder decodeIntForKey:#"1"];
return self;
}
- (void) encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeInt:highscore forKey:#"1"];
}
#end
Here is my mainAppDelegate.h:
#import <UIKit/UIKit.h>
#interface mainAppDelegate : UIResponder <UIApplicationDelegate>
#property (strong, nonatomic) UIWindow *window;
#property NSMutableArray *toDoItems;
#end
and my mainAppDelegate.m:
#import "mainAppDelegate.h"
#implementation mainAppDelegate
- (void)applicationWillTerminate:(UIApplication *)application
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"toDoItems.plist"];
[self.toDoItems writeToFile:filePath atomically:TRUE];
}
#end
I have another file, XYZToDoListViewController.m with the code:
#import "XYZToDoListViewController.h"
#import "XYZToDoItem.h"
#import "XYZAddItemViewController.h"
#interface XYZToDoListViewController ()
#property NSMutableArray *toDoItems;
#end
#implementation XYZToDoListViewController
- (IBAction)unwindToList:(UIStoryboardSegue *)segue
{
XYZAddItemViewController *source = [segue sourceViewController];
XYZToDoItem *item = source.toDoItem;
if (item != nil) {
[self.toDoItems addObject:item];
[self.tableView reloadData];
}
}
- (IBAction)clearData:(UIBarButtonItem *)sender;
{
[self.toDoItems removeAllObjects];
[self.tableView reloadData];
}
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.toDoItems = [[NSMutableArray alloc] init];
self.navigationController.view.backgroundColor =
[UIColor colorWithPatternImage:[UIImage imageNamed:#"bg_full.png"]];
self.tableView.backgroundColor = [UIColor clearColor];
self.tableView.contentInset = UIEdgeInsetsMake(-35, 0, -35, 0);
}
#end
This is at least what I think is relevant. My basic framework is something I followed from this tutorial. So as you can see I have an NSMutableArray named .toDoItems. and I want it to remember the list when I exit the application (not just minimizing it; it does that already. I think I have the code which is saving the data, but what do I use to check if the file exists, and if so, display it?
And would this affect my clearData method when the app resumes as well?
As if i understood true you want to save your NSMutableArray even after restart your app.
So you may use this codes to save your NSMutableArray with NSUserDefaults.
NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
[defaultsPhoneId setObject:yourArray forKey:#"yourKey"];
[defaultsPhoneId synchronize];
And it is how you can take it back
yourArray=[[NSMutableArray alloc]initWithArray:[[NSUserDefaults standardUserDefaults]objectForKey:#"yourKey"]];
But dont forget to initialize it before save with NSUserDefaults.
By the way you may check yourArray anytime if it is loaded or not with
if([yourArray count]==0){
//Your array is empty so you have to call it with #"yourKey". (second codes part)
}
"what do I use to check if the file exists"
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:your_file_path])
"..and if so, display it?"
Please read about UITableView and UITableViewDataSource
https://developer.apple.com/library/ios/documentation/uikit/reference/UITableView_Class/Reference/Reference.html
"And would this affect my clearData method when the app resumes as well?"
clearData is a button action. It will be called only if user will tap
a button. It is not connected with application launch/stopping.
I have class for playing sound in app.
I implemented on/off switch(on GUI), for disabling and enablining sound play.
I am using BOOL property for that and this is working.
Now I am trying to implement saving that BOOL (is sound on/off) in file so that next time when app is started state is automatically restored.
For that I am using NSCoding protocol, archiving is working but I have problem with unarchiving.
My app will not start it will just show black screen.
This is my code, only part that I think it is important.
GTN_Sound.h
#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h> // for playing sound
#interface GTN_Sound : NSObject <NSCoding>
#property(nonatomic, readwrite, unsafe_unretained) BOOL isSoundOn;
+ (id)sharedManager;
- (void)playWinSound;
- (void)playLoseSound;
#end
GTN_Sound.m
#pragma mark - NSCoding Methods
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeBool:self.isSoundOn forKey:#"isSoundOn"];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [GTN_Sound sharedManager];
if (self) {
_isSoundOn = [aDecoder decodeBoolForKey:#"isSoundOn"];
}
return self;
}
I think that code is so far so good ?
Continuation of GTN_Sound.m
#pragma mark - itemArchivePath Method
- (NSString *)itemArchivePath
{
// Make sure that the first argument is NSDocumentDirectory
// and not NSDocumentationDirectory
NSArray *documentDirectories =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
// Get the one document directory from that list
NSString *documentDirectory = [documentDirectories firstObject];
return [documentDirectory stringByAppendingPathComponent:#"sound.archive"];
}
#pragma mark - custom seter Method
- (void)setIsSoundOn:(BOOL)theBoolean {
NSLog(#"My custom setter\n");
if(_isSoundOn != theBoolean){
_isSoundOn = theBoolean;
NSString *path = [self itemArchivePath];
[NSKeyedArchiver archiveRootObject:self toFile:path]; // this is doing save
}
}
It is done that for every time when switch on GUI is changed I do the savings.
This look fine from my side, because I am not expecting that user will change this many times.
Now the unarchiving comes and I thin that here are some problems.
#pragma mark - Singleton Methods
+ (id)sharedManager {
static GTN_Sound *sharedMyManager = nil;
// to be thread safe
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] initPrivate];
});
return sharedMyManager;
}
- (instancetype)init
{
#throw [NSException exceptionWithName:#"Singleton"
reason:#"Use +[GTN_Sound sharedManager]"
userInfo:nil];
return nil;
}
// Here is the real (secret) initializer
- (instancetype)initPrivate
{
self = [super init];
if (self) {
NSString *path = [self itemArchivePath]; // do as iVar, for futture
self = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
//_isSoundOn = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
}
return self;
}
I think that problem is in this line
self = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
But have no idea how to fix it.
I have read that when I am doing archiving, that I need to archive all properties of object.
Does this apply to private ivars also ?
Any help is appreciated.
Thanks
You can only archive NSObject inheriting objects. ie. NSNumber
[NSKeyedArchiver archiveRootObject:#YES toFile:path]; //to save
_isSoundOn = [[NSKeyedUnarchiver unarchiveObjectWithFile:path] boolValue]; //to load
Ok, I've been over this a million times in the last week and I just am not getting it. (And yes, I've read Apple's docs.)
I am archiving my object and it appears to be archiving correctly (I can see the file written to the file system and if I examine it I can see my data within). However, when I relaunch my app my data is not being restored. Every example I read tells me how easy this is but I'm just not getting it. One unique thing is that my object is a singleton, it's used for passing data between view controllers.
I'd really appreciate some sage advice. Thanks in advance.
Here's my header:
#import <Foundation/Foundation.h>
#interface SharedAppDataObject : NSObject <NSCoding>
{
NSMutableDictionary *apiKeyDictionary;
NSString *skuFieldText;
NSIndexPath *checkmarkIndex;
}
+ (SharedAppDataObject *)sharedStore;
#property (nonatomic, copy) NSString *skuFieldText;
#property (nonatomic, copy) NSIndexPath *checkmarkIndex;
#property (nonatomic, copy) NSMutableDictionary *apiKeyDictionary;
-(void)setValue:(NSString *)apiKey forKey:(NSString *)name;
-(void)setSkuField:(NSString *)s;
-(void)setCheckmarkIndex:(NSIndexPath *)p;
-(NSMutableDictionary *)apiKeyDictionary;
-(BOOL)saveChanges;
#end
Here's my implementation:
#import "SharedAppDataObject.h"
#implementation SharedAppDataObject
#synthesize skuFieldText;
#synthesize checkmarkIndex;
#synthesize apiKeyDictionary;
//create our shared singleton store
+(SharedAppDataObject *)sharedStore {
static SharedAppDataObject *sharedStore = nil;
if (!sharedStore) {
sharedStore = [NSKeyedUnarchiver unarchiveObjectWithFile:[SharedAppDataObject archivePath]];
if(!sharedStore)
sharedStore = [[super allocWithZone:NULL] init];
}
return sharedStore;
}
-(id) init {
self = [super init];
if (self) {
}
return self;
}
-(void)setValue:(id)apiKey forKey:(NSString *)name {
[apiKeyDictionary setObject:apiKey forKey:name];
}
-(void)setSkuField:(NSString *)s {
skuFieldText = s;
}
-(NSMutableDictionary *)apiKeyDictionary {
return apiKeyDictionary;
}
-(void)setCheckmarkIndex:(NSIndexPath *)p {
checkmarkIndex = p;
}
-(void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:skuFieldText forKey:#"skuFieldText"];
[aCoder encodeObject:checkmarkIndex forKey:#"checkmarkIndex"];
[aCoder encodeObject:apiKeyDictionary forKey:#"apiKeyDictionary"];
}
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
[self setSkuFieldText:[aDecoder decodeObjectForKey:#"skuFieldText"]];
[self setCheckmarkIndex:[aDecoder decodeObjectForKey:#"checkmarkIndex"]];
[self setApiKeyDictionary:[aDecoder decodeObjectForKey:#"apiKeyDictionary"]];
}
return self;
}
+(NSString *)archivePath {
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
return [documentDirectory stringByAppendingPathComponent:#"bbyo.archive"];
}
-(BOOL)saveChanges {
return [NSKeyedArchiver archiveRootObject:self toFile:[SharedAppDataObject archivePath]];
}
#end
Save method from App Delegate:
- (void)applicationDidEnterBackground:(UIApplication *)application
{
BOOL success = [[SharedAppDataObject sharedStore] saveChanges];
if (success) {
NSLog(#"Saved all the data");
} else {
NSLog(#"Didn't save any of the data");
}
}
Initialize sharedStore = [NSKeyedUnarchiver unarchiveObjectWithFile:[SharedAppDataObject archivePath]]; in application:didFinishLaunchingWithOptions:. This method is used to initialize data structures and restore previous app state.
Also, take out static SharedAppDataObject *sharedStore = nil; from sharedStore. If the save file exists, [ShareAppDataObject sharedStore] will always unarchive the file which is not necessary. It can be unarchived once during initialization.
Here's a post that can answer your problem: http://bit.ly/PJO8fM
I cannot give you the answer but some ideas to figure this out. Taking this line:
sharedStore = [NSKeyedUnarchiver unarchiveObjectWithFile:[SharedAppDataObject archivePath]];
So if the sharedStore is nil, something is wrong - so test for it. If nothing then log the path, and use NSFileManager methods to see if the file is there, its size etc. If you find the file is there and has size, but you cannot unarchive it, that's a problem of course. In that case, add special debug code just after you create the file:
-(BOOL)saveChanges {
BOO ret = [NSKeyedArchiver archiveRootObject:self toFile:[SharedAppDataObject archivePath]];
id foo = [NSKeyedUnarchiver unarchiveObjectWithFile:[SharedAppDataObject archivePath]];
// check if foo is not nil, if its the proper class, etc.
}
If when you save the file you can unarchive it just fine, but cannot on restart of the app, then something is wrong with the file. All this info should point the way to a solution.
Another thought - when you encode the data, log it, just to be sure its not nil - but even if so the unarchive should work.