NSCoding -decoder returns wrong values -iOS - ios

I have written the following method to encode/decode data..
- (void) encode: (BOOL) encodeBool int: (NSNumber *) integer boolean:(BOOL) boolean key: (NSString *) keyStr {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *gameStatePath = [documentsDirectory stringByAppendingPathComponent:#"gameData"];
if (encodeBool == YES) {
NSMutableData *gameData = [NSMutableData data];
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:gameData];
if (integer) {
[encoder encodeInt:[integer intValue] forKey:keyStr];
}
else if (boolean) {
[encoder encodeBool:boolean forKey:keyStr];
}
[encoder finishEncoding];
[gameData writeToFile:gameStatePath atomically:YES];
[encoder release];
} else {
NSMutableData *gameData = [NSData dataWithContentsOfFile:gameStatePath];
if (gameData) {
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:gameData];
if (integer) {
NSLog(#"%d", [decoder decodeIntForKey:keyStr]);
}
else if (boolean) {
if ([decoder decodeBoolForKey:keyStr]==YES) {
NSLog(#"YES");
} else {
NSLog(#"NO");
}
}
[decoder finishDecoding];
[decoder release];
}
}
}
And some testing
[[GameData sharedData] encode:YES int: [NSNumber numberWithInt:100] boolean:NO key:#"testInt"];
[[GameData sharedData] encode:YES int:nil boolean:YES key:#"bool"];
[[GameData sharedData] encode:YES int:[NSNumber numberWithInt:1030] boolean:nil key:#"test"];
[[GameData sharedData] encode:NO int: [NSNumber numberWithInt:1] boolean:nil key:#"testInt"];
[[GameData sharedData] encode:NO int:nil boolean:YES key:#"bool"];
[[GameData sharedData] encode:NO int:[NSNumber numberWithInt:100] boolean:nil key:#"test"];
and output is
0
NO
1030
only the last one is correct.. Can someone tell me what I am doing wrong? Thanks

Your problem is that every time you call your method, you overwrite the file - erasing the values you encoded in previous calls. You should probably rewrite your method so that you encode all the values in a single call.
One alternative is to create a GameState object and have it implement NSCoding, then read and serialize it with +[NSKeyedArchiver archiveRootObject:toFile:] and deserialize it with +[NSKeyedUnarchiver unarchiveObjectWithFile:]. The code to do so looks a bit like this:
#interface GameState : NSObject <NSCoding>
#property (nonatomic) int someInt;
#property (nonatomic) BOOL someBool;
#property (nonatomic, strong) NSString *someString;
#end
static NSString *const BoolKey = #"BoolKey";
static NSString *const StringKey = #"StringKey";
static NSString *const IntKey = #"IntKey";
#implementation GameState
- (id)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
_someBool = [coder decodeBoolForKey:BoolKey];
_someInt = [coder decodeIntForKey:IntKey];
_someString = [coder decodeObjectForKey:StringKey];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeBool:self.someBool forKey:BoolKey];
[aCoder encodeInt:self.someInt forKey:IntKey];
[aCoder encodeObject:self.someString forKey:StringKey];
}
#end
// Somewhere in your app where reading and saving game state is needed...
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = nil;
if ([paths count]) {
documentsDirectory = paths[0];
}
NSString *archivePath = [documentsDirectory stringByAppendingPathComponent:#"archive"];
GameState *gameState = [NSKeyedUnarchiver unarchiveObjectWithFile:archivePath];
if (!gameState) {
gameState = [[GameState alloc] init];
gameState.someString = #"a string";
gameState.someInt = 42;
gameState.someBool = YES;
}
// Make changes to gameState here...
[NSKeyedArchiver archiveRootObject:gameState toFile:archivePath];

The first problem is when you test if (boolean) it's the same as saying if (boolean == YES). Bools aren't objects, and can't be nil. When you pass nil in as a bool, it's the same as passing NO in. I don't think this accounts for all of your issues though. I think the file is not saving as well.
From the NSKeyedUnarchiver docs:
If you invoke one of the decode... methods of this class using a key
that does not exist in the archive, a non-positive value is returned.
This value varies by decoded type. For example, if a key does not
exist in an archive, decodeBoolForKey: returns NO, decodeIntForKey:
returns 0, and decodeObjectForKey: returns nil.
These are the erroneous values you're getting. To start, I note that you're not doing any error checking. Try adding some checks to see what's failing, for example, you could try:
[encoder finishEncoding];
NSError *error;
BOOL success = [gameData writeToFile:gameStatePath options:NSDataWritingAtomic error:&error];
if (success == NO) NSLog(#"Error: %#", [error localizedDescription]);
Once you get an error, we can go from there.

Related

Unarchive custom object with secure coding in iOS

I'm encoding an array of objects that conform to NSSecureCoding successfully like this.
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array requiringSecureCoding:YES error:nil];
How do I unarchive this data? I'm trying like this but it's returning nil.
NSError *error = nil;
NSArray<MyClass *> *array = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSArray class] fromData:data error:&error];
This is the error it returns.
The data couldn’t be read because it isn’t in the correct format.
Here is a simple example
#interface Secure:NSObject<NSSecureCoding> {
NSString* name;
}
#property NSString* name;
#end
#implementation Secure
#synthesize name;
// Confirms to NSSecureCoding
+ (BOOL)supportsSecureCoding {
return true;
}
- (void)encodeWithCoder:(nonnull NSCoder *)coder {
[coder encodeObject:self.name forKey: #"name"];
}
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
self = [self init];
if (self){
self.name = [coder decodeObjectOfClass:[NSString class] forKey:#"name"];
}
return self;
}
#end
Usage
Secure *archive = [[Secure alloc] init];
archive.name = #"ARC";
NSArray* array = #[archive];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array requiringSecureCoding:YES error:nil];
NSArray<Secure *> *unarchive = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSObject class] fromData:data error:nil];
NSLog(#"%#", unarchive.firstObject.name);

Unable to save table data via NSCoding

I have an array of Objects (containing an item name and a creation date) used to display table data. I want to save this array using NSCoding protocol. I call my saveDataToDisk method every time a "submit" button is pressed to submit a new table entry. However, my app crashes upon hitting submit. This is my first time learning how to save app data and I looked a several tutorials and my form seems to be the same. I also looked for several days on the internet and couldn't find a solution. What is wrong?
Here are my methods:
Decoder Method:
- (id)initWithCoder:(NSCoder *)decoder {
self = [super initWithCoder:decoder];
if (!self) {
return nil;
}
self.itemArray = [decoder decodeObjectForKey:#"itemArray"];
return self;
}
Encoder Method:
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.itemArray forKey:#"itemArray"];
}
Path Finder Method:
- (NSString*) pathForDataFile
{
NSFileManager * fileManager = [NSFileManager defaultManager];
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [documentsPath stringByAppendingPathComponent:#"appData"];
if([fileManager fileExistsAtPath:filePath] == NO)
{
[fileManager createDirectoryAtPath:filePath withIntermediateDirectories:NO attributes:nil error:nil];
}
NSString *fileName = #"Budgetlist.appData";
return [filePath stringByAppendingPathComponent: fileName];
}
Save Data Method:
- (void) saveDataToDisk
{
NSString * path = [self pathForDataFile];
NSMutableDictionary * rootObject;
rootObject = [NSMutableDictionary dictionary];
[rootObject setValue: [self itemArray] forKey:#"itemArray"];
[NSKeyedArchiver archiveRootObject: rootObject toFile: path];
}
Load Data Method:
- (void) loadDataFromDisk
{
NSString * path = [self pathForDataFile];
NSDictionary * rootObject;
rootObject = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
[self setItemArray: [rootObject valueForKey:#"itemArray"]];
}
My Button:
- (IBAction)submitButton:(id)sender {
if (self.textField.text.length > 0 && [self.textField.text floatValue]>0) {
self.item = [[Data alloc] init];
self.item.itemName = self.textField.text;
[self.itemArray addObject:self.item];
[self.tableView reloadData];
[self saveDataToDisk];
}
}
You are trying to save an NSArray with custom objects.
Your objects must implement NSCoding protocol as well.
Implement those:
- (id)initWithCoder:(NSCoder *)decoder
- (void)encodeWithCoder:(NSCoder *)encoder
in your "Data" class.
NSArray is going through all objects and call those methods for each object inisde an array

NSCoding: Custom Class unarchived, but with empty/nil class members

I want to persist an NSDictionary, filled with custom Object to disc:
NSDictionary *menuList = [[NSMutableDictionary alloc]initWithDictionary:xmlParser.items];
//here the "Menu List"`s Object are filled correctly
//persisting them to disc:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *directory = [paths objectAtIndex:0];
NSString *fileName = [NSString stringWithUTF8String:MENU_LIST_NAME];
NSString *filePath = [directory stringByAppendingPathComponent:fileName];
//saving using NSKeyedArchiver
NSData* archiveData = [NSKeyedArchiver archivedDataWithRootObject:menuList];
[archiveData writeToFile:filePath options:NSDataWritingAtomic error:nil];
//here the NSDictionary has the correct amount of Objects, but the Objects` class members are partially empty or nil
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSDictionary *theMenu = (NSDictionary*)[NSKeyedUnarchiver unarchiveObjectWithData:data];
Here the .m of the Custom Object (the kind of object stored in the NSDictionary)
- (id)initWithTitle:(NSString*)tTitle level:(NSString*)tLevel state:(NSString*)tState visible:(BOOL)tVisible link:(NSString*)tLink linkType:(NSString*)tLinkType anId:(NSString*)tId {
if ((self = [super init])) {
self.anId = tId;
self.title = tTitle;
self.level = tLevel;
self.state = tState;
self.visible = tVisible;
self.link = tLink;
self.linkType = tLinkType;
}
return self;
}
- (void) encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.anId forKey:#"anId"];
[encoder encodeObject:self.level forKey:#"level"];
[encoder encodeObject:self.state forKey:#"state"];
[encoder encodeBool:self.visible forKey:#"visible"];
[encoder encodeObject:self.title forKey:#"title"];
[encoder encodeObject:self.link forKey:#"link"];
[encoder encodeObject:self.linkType forKey:#"linkType"];
}
- (id)initWithCoder:(NSCoder *)decoder {
if(self == [super init]){
self.anId = [decoder decodeObjectForKey:#"anId"];
self.level = [decoder decodeObjectForKey:#"level"];
self.state = [decoder decodeObjectForKey:#"state"];
self.visible = [decoder decodeBoolForKey:#"visible"];
self.title = [decoder decodeObjectForKey:#"title"];
self.link = [decoder decodeObjectForKey:#"link"];
self.linkType = [decoder decodeObjectForKey:#"linkType"];
}
return self;
}
#end
I dont know why the objects are unarchived correctly, but the object´s members are lost somewhere. I assume there must be an error somewhere in the NSCoding methods, but I just can´t find it, any help much appreciated.
When you implement the initWithCoder: method, you need to call super properly:
if (self = [super initWithCoder:decoder]) {
The instance that is being unarchived has more attributes than just the additions in your specific class. You also don't want to be checking equality with self, you want to be assigning to self.

Writing and reading custom object to file IOS

i have this object.
#interface SeccionItem : NSObject <NSCoding>
{
NSString * title;
NSString * texto;
NSArray * images;
}
#property (nonatomic,strong) NSString * title;
#property (nonatomic,strong) NSString * texto;
#property (nonatomic,strong) NSArray * images;
#end
With this implementation
#implementation SeccionItem
#synthesize title,texto,images;
- (void) encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:title forKey:#"title"];
[encoder encodeObject:texto forKey:#"texto"];
[encoder encodeObject:images forKey:#"images"];
}
- (id)initWithCoder:(NSCoder *)decoder {
title = [decoder decodeObjectForKey:#"title"];
texto = [decoder decodeObjectForKey:#"texto"];
images = [decoder decodeObjectForKey:#"images"];
return self;
}
#end
I want to save an array filled with this objects to a file on disk.
Im doing this:
to write
[NSKeyedArchiver archiveRootObject:arr toFile:file];
to read
NSArray *entries = [NSKeyedUnarchiver unarchiveObjectWithFile:name];
return entries;
But the readed array is always empty, i dont know why, i have some questions.
What format should i use for file path? on toFile:?
The NSArray on the object is filled with NSData objects, so i can encode them?
Im really lost on this.
Take a look at the documentation of NSKeyedArchiver, especially the archiveWithRootObject:toFile: method.
The path is basically where the file should be stored including the file name. For example you can store your array in your app Documents folder with file name called Storage. The code snippet below is quite common:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docDir = [paths objectAtIndex: 0];
NSString* docFile = [docDir stringByAppendingPathComponent: #"Storage"];
The method NSSearchPathForDirectoriesInDomains is used instead of absolute path because Apple can be changing the Documents folder path as they want it.
You can use the docFile string above to be supplied to the toFile parameter of the archiveWithRootObject:toFile: method.
Use the following method to save data
-(NSString*)saveFilePath {
NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *pathString = [[pathArray objectAtIndex:0] stringByAppendingPathComponent:#"data"];
//NSString *pathString = [[NSBundle mainBundle]pathForResource:#"Profile" ofType:#"plist"];
return pathString;
}
-(void)saveProfile {
SeccionItem *data = [[SeccionItem alloc]init]
data. title = #"title";
data. texto = #"fdgdf";
data.images = [NSArray arrayWithObjects:#"dfds", nil];
NSMutableData *pData = [[NSMutableData alloc]init];
NSString *path = [self saveFilePath];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]initForWritingWithMutableData:pData];
[data encodeWithCoder:archiver];
[archiver finishEncoding];
[pData writeToFile:path atomically:YES];
}
Use the following method to load data
-(void)loadData {
NSString* path = [self saveFilePath];
//NSLog(path);
NSMutableData *pData = [[NSMutableData alloc]initWithContentsOfFile:path];
NSKeyedUnarchiver *unArchiver = [[NSKeyedUnarchiver alloc]initForReadingWithData:pData];
data = [[SeccionItem alloc]initWithCoder:unArchiver];
//NSLog(#"%#",data.firstName);
[unArchiver finishDecoding];
}
For those who are seeking the solution in swift, I was able to write and read dictionary to file system as follows :
Write:
let data = NSKeyedArchiver.archivedData(withRootObject: dictionary)
do {
try data.write(to: destinationPath)
} catch let error {
print("\(error.localizedDescription)")
}
Read:
do
{
let data = try Data.init(contentsOf: path)
// path e.g. file:///private/var/ .... /Documents/folder/filename
if let dict = NSKeyedUnarchiver.unarchiveObject(with: data){
return dict
}
}
catch let error
{
print("\(error.localizedDescription)")
}

Using a class method to create an NSArray

Once more I come to the Internet, hat in hand. :)
I'm attempting to use a class method to return a populated array containing other arrays as elements:
.h:
#interface NetworkData : NSObject {
}
+(NSString*) getCachePath:(NSString*) filename;
+(void) writeToFile:(NSString*)text withFilename:(NSString*) filePath;
+(NSString*) readFromFile:(NSString*) filePath;
+(void) loadParkData:(NSString*) filename;
+(NSArray*) generateColumnArray:(int) column type:(NSString*) type filename:(NSString*) filename;
#end
.m:
#import "NetworkData.h"
#import "JSON.h"
#import "Utility.h"
#implementation NetworkData
+(NSString*) getCachePath:(NSString*) filename {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *cachePath = [NSString stringWithFormat:#"%#/%#", [paths objectAtIndex:0], filename];
[paths release];
return cachePath;
}
+(void) writeToFile:(NSString*)text withFilename:(NSString*) filename {
NSMutableArray *array = [[NSArray alloc] init];
[array addObject:text];
[array writeToFile:filename atomically:YES];
[array release];
}
+(NSString*) readFromFile:(NSString*) filename {
NSFileManager* filemgr = [[NSFileManager alloc] init];
NSData* buffer = [filemgr contentsAtPath:filename];
NSString* data = [[NSString alloc] initWithData:buffer encoding:NSUTF8StringEncoding];
[buffer release];
[filemgr release];
return data;
}
+(void) loadParkData:(NSString*) filename {
NSString *filePath = [self getCachePath:filename];
NSURL *url = [NSURL URLWithString:#"http://my.appserver.com"];
NSData *urlData = [NSData dataWithContentsOfURL:url];
[urlData writeToFile:filePath atomically:YES];
}
+(NSArray*) generateColumnArray:(int) column type:(NSString*) type filename:(NSString*) filename {
// NSLog(#"generateColumnArray called: %u %# %#", column, type, filename);
// productArray = [[NSMutableArray alloc] init];
// NSString *filePath = [self getCachePath:filename];
// NSString *fileContent = [self readFromFile:filePath];
// NSString *jsonString = [[NSString alloc] initWithString:fileContent];
// NSDictionary *results = [jsonString JSONValue];
// NSArray *eventsArray = [results objectForKey:type];
// NSInteger* eventsArrayCount = [eventsArray count];
// NSInteger* a;
// for (a = 0; a < eventsArrayCount; a++) {
// NSArray *eventsColSrc = [eventsArray objectAtIndex:a];
// NSArray *blockArray = [eventsColSrc objectAtIndex:column];
// [productArray addObject:blockArray];
// [blockArray release];
// }
// [eventsArray release];
// [results release];
// [jsonString release];
// [fileContent release];
// [filePath release];
// [a release];
// [eventsArrayCount release];
// return productArray;
}
-(void)dealloc {
[super dealloc];
}
#end
.. and the call:
NSArray* dataColumn = [NetworkData generateColumnArray:0 type:#"eventtype_a" filename:#"data.json"];
The code within the method works (isn't pretty, I know - noob at work). It's essentially moot because just calling it (with no active code, as shown) causes the app to quit before the splash screen reveals anything else.
I'm betting this is a headslapper - many thanks for any knowledge you can drop.
If your app crashes, there's very likely a message in the console that tells you why. It's always helpful to include that message when seeking help.
One obvious problem is that your +generateColumnArray... method is supposed to return a pointer to an NSArray, but with all the code in the method commented out, it's not returning anything, and who-knows-what is being assigned to dataColumn. Try just adding a return nil; to the end of the method and see if that fixes the crash. Again, though, look at the error message to see specifically why the code is crashing, and that will lead you to the solution.
Well, you're not returning a valid value from your commented out code. What do you use 'dataColumn' for next? Running under the debugger should point you right to the issue, no?

Resources