Writing NSMutableArrray to file an then reading it - iphone

I have a NSMutableArray feed.leagues which has two objects of <MLBLeagueStandings: 0xeb2e4b0>
I want to write it to a file and then read it from the file. This is what I have done:
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:feed.leagues forKey:#"feed.leagues"];
}
- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
self.feed.leagues = [decoder decodeObjectForKey:#"feed.leagues"];
}
return self;
}
-(void)saveJSONToCache:(NSMutableArray*)leaguesArray {
NSString *cachePath = [self cacheJSONPath];
[NSKeyedArchiver archiveRootObject:feed.leagues toFile:cachePath];
NSMutableArray *aArray = [NSKeyedUnarchiver unarchiveObjectWithFile:cachePath];
NSLog(#"aArray is %#", aArray);
}
-(NSString*)cacheJSONPath
{
NSString *documentsDirStandings = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *cacheJSONPath = [NSString stringWithFormat:#"%#/%#_Standings.plist",documentsDirStandings, sport.acronym];
return cacheJSONPath;
}

Your object : MLBLeagueStandings should be serializable and respond to NSCoding protocole :
#interface MLBLeagueStandings : NSObject <NSCoding>{
}
Now in your MLBLeagueStandings class file (.m) add the following methods :
- (id)initWithCoder:(NSCoder *)decoder;
{
self = [super initWithCoder:decoder];
if(self)
{
yourAttribute = [decoder decodeObjectForKey:#"MY_KEY"]
//do this for all your attributes
}
}
- (void)encodeWithCoder:(NSCoder *)encoder;
{
[encoder encodeObject:yourAttribute forKey:#"MY_KEY"];
//do this for all your attributes
}
In fact if you want to write an object to a file (in your case it's an array), all the object contained in this array have to conform to the NSCoding protocole.
Moreover if you want example : here is a good tutorial
Hope it will help you.
NB : if you want to encode/decode primitive type (int, float etc...) use :
[encode encodeInt:intValue forKey:#"KEY"];
(more information on apple doc)

Related

How to serialize a class in IOS sdk (Objective-c)?

How to serialize the following class in objective-c so that it can be used with SBJson?
I get "JSON serialisation not supported for Animal" error when I use this code.
Can someone point out where I am going wrong?
The contents of Animal.h file is as below
#import <UIKit/UIKit.h>
#interface Animal : NSObject<NSCoding> {
NSString *name;
NSString *description;
NSString *imageURL;
}
#property (nonatomic, retain) NSString *name;
#property (nonatomic, retain) NSString *description;
#property (nonatomic, retain) NSString *imageURL;
-(id)initWithName:(NSString *)n description:(NSString *)d url:(NSString *)u;
#end
The contents of Animal.m file is as below
#import "Animal.h"
#implementation Animal
#synthesize name, description, imageURL;
-(id)initWithName:(NSString *)n description:(NSString *)d url:(NSString *)u {
self.name = n;
self.description = d;
self.imageURL = u;
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if(self = [super initWithCoder:aDecoder])
{
name = [[aDecoder decodeObjectForKey:#"name"] retain];
description = [[aDecoder decodeObjectForKey:#"description"] retain];
imageURL = [[aDecoder decodeObjectForKey:#"imageURL"] retain];
}
return [self initWithName:name description:description url:imageURL];
}
- (void)encodeWithCoder:(NSCoder *)encoder
{
[super encodeWithCoder:encoder];
[encoder encodeObject:name forKey:#"name"];
[encoder encodeObject:description forKey:#"description"];
[encoder encodeObject:imageURL forKey:#"imageURL"];
}
#end
Make your custom class conform to NSCoding protocol and then serialize it.
For more info, visit the Apple documentation
Also, this link will also help you.
As suggested in this link, archive your custom class to NSData and serialize that as provided in the Apple documentation.
Edit
Make your Animal.m as follows:
#import "Animal.h"
#implementation Animal
#synthesize name, description, imageURL;
-(id)initWithName:(NSString *)n description:(NSString *)d url:(NSString *)u {
self = [super init];
if( self )
{
self.name = n;
self.description = d;
self.imageURL = u;
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if( self )
{
self.name = [aDecoder decodeObjectForKey:#"name"];
self.description = [aDecoder decodeObjectForKey:#"description"];
self.imageURL = [aDecoder decodeObjectForKey:#"imageURL"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:name forKey:#"name"];
[encoder encodeObject:description forKey:#"description"];
[encoder encodeObject:imageURL forKey:#"imageURL"];
}
#end
To actually answer your question on how to do it using SBJson: Implement the proxyForJson method. Unless you are serializing an NSArray or NSDictionary you must override this method for serialization to work.
You can see that this is the case by looking at the SBJson source code (in SBJsonWriter.m):
- (NSData*)dataWithObject:(id)object {
...
if ([object isKindOfClass:[NSDictionary class]])
ok = [streamWriter writeObject:object];
else if ([object isKindOfClass:[NSArray class]])
ok = [streamWriter writeArray:object];
else if ([object respondsToSelector:#selector(proxyForJson)])
return [self dataWithObject:[object proxyForJson]];
else {
self.error = #"Not valid type for JSON";
return nil;
}
...
}
}
Implement proxyForJson in Animal.m like this (not tested):
- (NSDictionary*) proxyForJson
{
return [NSDictionary dictionaryWithObjectsAndKeys:self.name, #"name",
self.description, #"description",
self.imageURL, #"imageURL",
nil];
}
This open source project JSONCoding makes the whole process pretty simple, using the new sdk class in conjunction with the NSCoding protocol.
Check the newly introduced NSJSONSerialization class:
NSJSONSerialization class
I think you can check out this if it helps you: Make a Custom Class Serializable
Please check this Property List Programming Guide - Serializing a Property List
and the similar post here:
Make a Custom Class Serializable in Objective-c/iPhone?
Object serialization in XML format using Obj-C / iPhone SDK
See also https://github.com/jsonmodel/jsonmodel
Magical Data Modeling Framework for JSON - allows rapid creation of
smart data models. You can use it in your iOS, macOS, watchOS and tvOS
apps.
This is also the chosen library for the Objective-c Swagger client.

Why is only one attribute of my <NSCoding> object being properly written to a file?

So I'm trying to write a NSMutableArray of custom objects (a "Course" representing a college course for a Course Planner app) to a file when my application terminates and then read that array from the file into the relevant ViewController that will make use of the data when the application starts up.
Here is the relevant code:
CoursesAppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
coursesViewController = [[SampleHomeScreen alloc] initWithNibName:#"SampleHomeScreen" bundle:nil];
NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
[coursesViewController setCourses:[NSKeyedUnarchiver unarchiveObjectWithFile: filePath]];
}
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(applicationWillTerminate:)name:UIApplicationWillTerminateNotification object:app];
[window addSubview:coursesViewController.view];
[window makeKeyAndVisible];
return YES;
}
- (NSString *)dataFilePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:#"data.plist"];
NSLog(#"%#", path);
return path;
}
/**
applicationWillTerminate: saves changes in the application's managed object context before the application terminates.
*/
- (void)applicationWillTerminate:(UIApplication *)application {
NSLog(#"%#", [coursesViewController courses]);
[NSKeyedArchiver archiveRootObject:[coursesViewController courses] toFile:[self dataFilePath]];
}
Course.h:
#interface Course : NSObject <NSCoding> {
NSString *name; //e.g. ECS 189H
double grade, totalWeight; //course grade in %
NSMutableArray *list;
}
#property (nonatomic, retain) NSString *name;
#property (nonatomic) double grade, totalWeight;
#property (nonatomic, retain) NSMutableArray *list;
-(Course *)initWithName:(NSString *)courseName;
#end
Course.m:
#implementation Course
#synthesize name, grade, totalWeight, list;
-(Course *)initWithName:(NSString *)courseName {
name = [courseName retain];
grade = -1.0;
totalWeight = 0.0;
list = [[NSMutableArray alloc] init];
[super init];
return self;
}
-(Course *)initWithCoder:(NSCoder *)aDecoder {
self.name = [aDecoder decodeObjectForKey:#"name"];
self.grade = [aDecoder decodeDoubleForKey:#"grade"];
self.totalWeight = [aDecoder decodeDoubleForKey:#"totalWeight"];
self.list = [aDecoder decodeObjectForKey:#"list"];
[super init];
return self;
}
- (void) encodeWithCoder: (NSCoder *)coder
{
[coder encodeObject:name forKey:#"name"];
[coder encodeDouble:grade forKey:#"grade"];
[coder encodeDouble:totalWeight forKey:#"totalWeight"];
[coder encodeObject:list forKey:#"list"];
}
-(void)dealloc {
[name release];
[list release];
[super dealloc];
}
#end
[coursesViewController courses] is the NSMutableArray that holds the course objects. I know for a fact that it holds valid data.
So the problems are,
1: The application saves to data.plist ONLY when I run it from xcode (ie click "compile and run" in xcode).
2: It loads data from the plist, but all that gets saved are the course names and the default values for grade and totalWeight (-1 and 0 respectively). So really they are saved as though initWithName was called on them first.
This is my first real delve into a fairly advanced iOS application, so as I am a newbie to this, I may have left out some important info. If that is the case, please let me know and I will update the question.
Thanks!
-HT
p.s. If it is relevant, I have doNotRunInBackground in the info.plist set to true.
Your are trying to set values in your object before it's been initialized. And initialization will then reset your values.
-(Course *)initWithName:(NSString *)courseName {
name = [courseName retain]; // <- Accessing ivar before self is initialized
grade = -1.0; // <- Accessing ivar before self is initialized
totalWeight = 0.0; // <- Accessing ivar before self is initialized
list = [[NSMutableArray alloc] init]; // <- Accessing ivar before self is initialized
[super init]; // initialization resets your values !!!!
return self;
}
Additionally you are ignoring super's init return value, which will work fine 98 % of all cases, but I recommend to always use a proper initialization scheme:
- (id)init {
if (self = [super init]) {
// It's save to access ivars here
}
return self
}
In Cocoa an init method may return a different object, then the one that was allocated. So you must assign self to the super's init.
So, your init should look like:
-(Course *)initWithName:(NSString *)courseName {
if (self = [super init]) {
name = [courseName retain];
grade = -1.0;
totalWeight = 0.0;
list = [[NSMutableArray alloc] init];
}
return self;
}
The same applies to initWithCoder:(NSCoder *)coder.

Save own Class with NSCoder

I'm trying to store some custom class/data to a file in my iPhone/iPad app.
I have a Class RSHighscoreList
#interface RSHighscoreList : NSObject {
NSMutableArray *list;
}
which contains objects of RSHighscore in the list
#interface RSHighscore : NSObject {
NSString *playerName;
NSInteger points;
}
When I try to store all to file
- (void)writeDataStore {
RSDataStore *tmpStore = [[RSDataStore alloc] init];
_tmpStore.highscorelist = self.highscorelist.list;
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:tmpStore forKey:kDataKey];
[archiver finishEncoding];
[data writeToFile:[self dataFilePath] atomically:YES];
[archiver release];
[data release];
}
#interface RSDataStore : NSObject <NSCoding, NSCopying> {
NSMutableArray *highscorelist;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:highscorelist forKey:#"Highscorelist"];
}
The app will crash with an error message
-[RSHighscore encodeWithCoder:]: unrecognized selector sent to instance 0x573cc20
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[RSHighscore encodeWithCoder:]: unrecognized selector sent to instance 0x573cc20'
I wonder why the error tells about RSHighscore, even if that is 'wrapped'. Does anyone have a good idea?
RSDataStore has an -encodeWithCoder: method, but (according to the error message) RSHighscore doesn't. You need to implement the NSCoding protocol for every class you're serializing.
#implementation RSHighscore
static NSString *const kPlayerName = #"PlayerName";
static NSString *const kPoints = #"Points";
-(id)initWithCoder:(NSCoder *)decoder {
if ((self=[super init])) {
playerName = [[decoder decodeObjectForKey:kPlayerName] retain];
points = [decoder decodeIntegerForKey:kPoints];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:playerName forKey:kPlayerName];
[encoder encodeInt:points forKey:kPoints];
}
...
If the base class of RSHighscore is ever changed to something other than NSObject, the -initWithCoder: method might need to be changed to call [super initWithCoder:decoder] rather than [super init]. Alternatively, add <NSCoding> to NSObject and change RSHighscore's -initWithCoder: now.
#interface NSObject (NSCoding)
-(id)initWithCoder:(NSCoder*)decoder;
-(void)encodeWithCoder:(NSCoder*)encoder;
#end
#implementation NSObject (NSCoding)
-(id)initWithCoder:(NSCoder*)decoder {
return [self init];
}
-(void)encodeWithCoder:(NSCoder*)encoder {}
#end
#implementation RSHighscore
-(id)initWithCoder:(NSCoder *)decoder {
if ((self=[super initWithCoder:decoder])) {
playerName = [[decoder decodeObjectForKey:kPlayerName] retain];
points = [decoder decodeIntegerForKey:kPoints];
}
return self;
}
...
The class you're going to encode or initWithCoder should conform to <NSCoding> protocol
So you just should add this in your interface, otherwise indeed the runtime will not recognize the selector as it's the part of <NSCoding> protocol

How to use NSCoder

I am developing iphone application.
I use NSCoder.
MyApplication.h
#define ITEMS_KEY #"items"
#define CATEGORIES_KEY #"categories"
#import <UIKit/UIKit.h>
#interface MyApplicationData : NSObject <NSCoding, NSCopying> {
NSMutableArray* items;
NSMutableArray* categories;
}
#property (nonatomic ,retain) NSMutableArray* items;
#property (nonatomic, retain) NSMutableArray* categories;
#end
Myapplication.m
#import "MyApplicationData.h"
#implementation MyApplicationData
#synthesize items;
#synthesize categories;
#pragma mark NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:items forKey:ITEMS_KEY];
[aCoder encodeObject:categories forKey:CATEGORIES_KEY];
}
-(id)initWithCoder:(NSCoder *)aDecoder{
if(self = [super init]){
self.items = [aDecoder decodeObjectForKey:ITEMS_KEY];
self.categories = [aDecoder decodeObjectForKey:CATEGORIES_KEY];
}
return self;
}
#pragma mark -
#pragma mark NSCopying
-(id)copyWithZone:(NSZone *)zone{
MyApplicationData* copy = [[[self class]allocWithZone:zone]init];
items = [self.items copy];
categories = [self.categories copy];
return copy;
}
#end
But warnning.
'NSCoder' may not respond to '-decodeDataObjectForKey'
How to use NSCoder?
Use -decodeObjectForKey: and read the documentation.
Here is the NSCoding protocal methods from my LogEntry object, you can ignore the switch statement and the schema details though, those are from a base class I have written that allows me to keep sane track of changing data formats.
Please note that in addition to using decodeObjectForKey: you also need to make sure you retain/copy the given values as they are autoreleased when received.
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self != nil) {
switch ([schemaVersion intValue]) {
case 2:
filepath = [[coder decodeObjectForKey:#"filepath"] copy];
identifier = [coder decodeInt64ForKey:#"identifier"];
level = [coder decodeIntForKey:#"level"];
lineNumber = [[coder decodeObjectForKey:#"lineNumber"] retain];
message = [[coder decodeObjectForKey:#"message"] retain];
timestamp = [[coder decodeObjectForKey:#"timestamp"] retain];
break;
default:
[self release], self = nil;
break;
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:filepath forKey:#"filepath"];
[coder encodeInt64:identifier forKey:#"identifier"];
[coder encodeInt:level forKey:#"level"];
[coder encodeObject:lineNumber forKey:#"lineNumber"];
[coder encodeObject:message forKey:#"message"];
[coder encodeObject:timestamp forKey:#"timestamp"];
[super encodeWithCoder:coder];
}
I think you should be using -decodeObjectForKey:
I wrote a helper function for using NSCoding. It's a part of VSCore Library. Check it out here:
#interface QuickCoding : NSObject
+ (void)quickEncode:(NSObject<NSCoding>*)object withEncoder:(NSCoder*)encoder;
+ (void)quickDecode:(NSObject<NSCoding>*)object withDecoder:(NSCoder*)decoder;
#end
And the .m file:
#import "QuickCoding.h"
#import "ReflectionHelper.h"
#define QUICK_CODING_HASH #"h4"
#implementation QuickCoding
+ (void)quickEncode:(NSObject<NSCoding>*)object withEncoder:(NSCoder *)encoder{
NSArray *codingKeys = [ReflectionHelper fieldsList:[object class]];
NSUInteger hash = [[codingKeys componentsJoinedByString:#""] hash];
[encoder encodeObject:#(hash) forKey:QUICK_CODING_HASH];
[codingKeys enumerateObjectsUsingBlock:^(NSString *key, __unused NSUInteger idx, __unused BOOL *stop) {
id val = [object valueForKey:key];
if ([val conformsToProtocol:#protocol(NSCoding)]){
[encoder encodeObject:val forKey:key];
}
}];
}
+ (void)quickDecode:(NSObject<NSCoding>*)object withDecoder:(NSCoder *)decoder{
NSArray *codingKeys = [ReflectionHelper fieldsList:[object class]];
NSUInteger hash = [[codingKeys componentsJoinedByString:#""] hash];
NSUInteger decodedHash = [[decoder decodeObjectForKey:QUICK_CODING_HASH] unsignedIntegerValue];
BOOL equalHash = hash == decodedHash;
[codingKeys enumerateObjectsUsingBlock:^(NSString *key, __unused NSUInteger idx, __unused BOOL *stop) {
id val = [decoder decodeObjectForKey:key];
if (equalHash || val){
[object setValue:val forKey:key];
}
}];
}
#end
Full code is here: https://github.com/voipswitch/VSCore/tree/master/VSCore/Storage

Singleton Class iPhone

Ok, I'm trying to avoid global variables, so I read up on singleton classes.
This is a try to set and read a mutable array, but the result is null.
//Content.h
#interface Content : NSObject {
NSMutableArray *contentArray;
}
+ (Content *) sharedInstance;
- (NSMutableArray *) getArray;
- (void) addArray:(NSMutableArray *)mutableArray;
#end
.
//Content.m
#implementation Content
static Content *_sharedInstance;
+ (Content *) sharedInstance
{
if (!_sharedInstance)
{
_sharedInstance = [[Content alloc] init];
}
return _sharedInstance;
}
- (NSMutableArray *) getArray{
return contentArray;
}
- (void) addArray:(NSMutableArray *)mutableArray{
[contentArray addObject:mutableArray];
}
#end
And in a ViewController I added #import "Content.h", where I try to call this:
NSMutableArray *mArray = [NSMutableArray arrayWithObjects:#"test",#"foo",#"bar",nil];
Content *content = [Content sharedInstance];
[content addArray:mArray];
NSLog(#"contentArray: %#", [content getArray]);
You need to alloc and init the array first. Personally I'd do it in the init method of the content class like so:
-(id)init{
if(self = [super init]){
…the rest of your init code…
contentArray = [[NSMutableArray alloc] init];
}
return self;
}
You never actually alloc/initialise the contentArray array.