This has been asked before and people have given very good instructions on how to do this, e.g. here.
However, I was wondering if I really need to work with NSCoder if I simply wanted to save one NSMutableArray (containing various instances of another NSMutableArray) to a file? I tried this but only got an error message:
-(void)saveLibraryDat {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents directory
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"myLibrary.dat"];
NSError *error;
[myLibrary writeToFile:filePath atomically:YES];
if (error) {
NSLog(#"There was an error saving myLibrary.dat: %#", error);
}
}
My error message:
2011-05-13 22:00:47.840 MoleNotes[15437:207] There was an error saving myLibrary.dat: (
1,
2
)
So I guess I have to work with NSCoder, right? If so, I was wondering how to go about this. People have explained how to do this with a class, but in my case, I have a NSMutableArray (myLibrary) which contains various instances of a class. Will I have to implement the NSCoder in this class and the NSMutableArray?
I alloc my library like this:
myLibrary = [[NSMutableArray alloc] init];
And then add instances of a class called NoteBook.m like this:
NoteBook *newNoteBook = [[NoteBook alloc] init];
newNoteBook.titleName = #"Notes"; // etc.
[myLibrary addObject:newNoteBook];
So where exactly do I put the NSCoder commands? Only into my NoteBook.m class? Will this automatically take care of myLibrary?
Thanks for any suggestions.
EDIT:
So I've updated my code, but I guess the big problem is that my NSMutableArray myLibrary contains several instances of a custom class I've set up (called notebook). I have set up NSCoding for this class (and all its variables) so that I can save it and load it.
Now my app works totally fine if I create the NSMutableArray in the app (i.e. when the app is started for the very first time, no file exists), instead of loading it from disk:
-(void) setupLibrary {
myLibrary = [[NSMutableArray alloc] init];
NoteBook *newNoteBook = [[NoteBook alloc] init];
newNoteBook.titleName = #"Notes";
/...
If I load it from disk, it works fine as well:
-(void)loadLibraryDat {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents directory
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"myLibrary.dat"];
myLibrary = [[NSMutableArray alloc] init];
myLibrary = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
if (!myLibrary) {
// if it couldn't be loaded from disk create a new one
NSLog(#"myLibrary.dat empty... set up new one");
[self setupLibrary];
} else { NSLog(#"Loading myLibrary.dat successful."); }
}
If I log everything which is contained in my library after loading it, everything is still fine. E.g. the following works totally fine:
NSLog(#"%#", [[self.myLibrary objectAtIndex:0] titleName]);
The big problem is, however, if any other method tries to access myLibrary. For instance, if I call the very same log command from another method, the app will crash and I get this error message:
[NSCFString objectAtIndex:]: unrecognized selector sent to instance 0x4b38510
2011-05-14 14:09:10.490 Notes[17091:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSCFString objectAtIndex:]: unrecognized selector sent to instance 0x4b38510'
This sounds to me as if myLibrary has become deallocated somehow, but I can't see why. How could this have happened? I have the feeling that I did something wrong in my NSCoding set up... because if I simply create myLibrary in code, everything works like wonderfully. It's only if I load it from the disk, that the app will crash.
Here is the class setup:
#import <Foundation/Foundation.h>
#interface NoteBook : NSObject <NSCoding> {
NSString *titleName;
NSString *fileName;
NSMutableArray *tabTitles;
NSMutableArray *tabColours;
NSMutableArray *tabReference;
}
#property (nonatomic, retain) NSString *titleName;
#property (nonatomic, retain) NSString *fileName;
#property (nonatomic, retain) NSMutableArray *tabTitles;
#property (nonatomic, retain) NSMutableArray *tabColours;
#property (nonatomic, retain) NSMutableArray *tabReference;
-(id)initWithCoder:(NSCoder *)aDecoder;
-(void)encodeWithCoder:(NSCoder *)aCoder;
#end
//
// NoteBook.m
#import "NoteBook.h"
#implementation NoteBook
#synthesize titleName, fileName, tabTitles, tabColours, tabReference;
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
self.titleName = [aDecoder decodeObjectForKey:#"titleName"];
self.fileName = [aDecoder decodeObjectForKey:#"fileName"];
self.tabTitles = [aDecoder decodeObjectForKey:#"tabTitles"];
self.tabColours = [aDecoder decodeObjectForKey:#"tabColours"];
self.tabReference = [aDecoder decodeObjectForKey:#"tabReference"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:titleName forKey:#"titleName"];
[aCoder encodeObject:fileName forKey:#"fileName"];
[aCoder encodeObject:tabTitles forKey:#"tabTitles"];
[aCoder encodeObject:tabColours forKey:#"tabColours"];
[aCoder encodeObject:tabReference forKey:#"tabReference"];
}
#end
EDIT:
I think I've solved it... I forgot a little 'self'... which messed it all up and deallocated myLibrary:
self.myLibrary = [NSKeyedUnarchiver
unarchiveObjectWithFile:filePath];
if (self.myLibrary == nil) {
NSLog(#"myLibrary.dat empty... set up new one");
[self setupLibrary];
} else { NSLog(#"Loading myLibrary.dat successful."); }
Your code is busted. The "error" variable is uninitialized and never set, so when you check it, you're just seeing random garbage data. If you want to know whether the write was successful, check the return value of writeToFile:atomically:. It will be YES if the write succeeded and NO if it didn't.
However, NSArray's writeTo… methods are for creating plists. If non-property-list objects are in your array, that method isn't appropriate, and an archiver is what you want. Just do something like [[NSKeyedArchiver archivedDataWithRootObject:myLibrary] writeToFile:writeToFile:filePath atomically:YES].
To make your objects conform to NSCoding correctly, just have them implement initWithCoder: and encodeWithCoder:, and in those methods, use NSCoder's storage methods to store the object's instance variables (and the retrieval methods to get them back out).
NSCoder is a protocol that your class must conform to in order to be archived to data/file. Works something like Serealizabe in Java.
Add conformance to the class header like this:
#interface NoteBook : NSObject <NSCoder> { // …
And then you must implement two methods:
-(id)initWithCoder:(NSCoder)decoder;
{
self = [super initWithCoder:decoder];
if (self) {
_someIvar = [decoder decodeObjectForKey:#"someKey"];
// And more init as needed…
}
return self;
}
-(void)encodeWithCoder:(NSCoder)coder;
{
[super encodeWithCoder:coder];
[coder encodeObject:_someIvar forKey#"someKey"];
/// Etc…
}
I would also advice against using -[NSArray writeToFile:atomically:] since in work with property list compliant objects only, not coding compliant classes. The property list object are NSString, NSData, NSArray, or NSDictionary, NSDate, and NSNumber. The list can not be extended.
Instead use NSKeyedArchiver/NSKeyedUnarchiver. Almost as simple to use:
if (![NSKeyedArchive archiveRootObject:yourArrat toFile:path]) {
// It failed.
}
Related
In one of my methods, I fetched and parsed a JSON and placed it inside an NSArray called jsonArray in -(void)method1. I then copied the contents of that jsonArray to an NSMutableArray called copiedJsonArray to be used on other methods. Problem is, copiedJsonArray crashes whenever I log its contents in the console from the other methods -(void)method2 but it logs fine in -(void)method1.
How can I fix this?
In my header file:
#interface MainViewController : UIViewController
#property (nonatomic, retain) NSMutableArray *copiedJsonArray;
In my implementation file:
#synthesize copiedJsonArray;
- (void)viewDidLoad
{
[self method1];
}
- (void)method1
{
NSString *urlString = [NSString stringWithFormat:THE_URL];
NSURL *url = [NSURL URLWithString:urlString];
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *jsonString = [[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding] autorelease];
NSDictionary *jsonDictonary = [jsonString JSONValue];
NSArray *jsonArray = [jsonDictonary valueForKeyPath:#"QUERY.DATA"];
self.copiedJsonArray = [[NSMutableArray alloc] initWithArray:jsonArray copyItems:YES];
NSLog(#"Copied JSON Array in Method 1: %#", self.copiedJsonArray);
[self method2];
}
- (void)method2
{
NSLog(#"Copied JSON Array in Method 2: %#", self.copiedJsonArray);
}
I also tried doing this too but it does the same error:
copiedJsonArray = [jsonArray mutableCopy];
I also tried implementing NSCopy but fails too:
#interface MainViewController : UIViewController <NSCopying>
{
NSMutableArray *copiedJsonArray;
}
I'm doing this so that I can do a loop in my copiedJsonArray without fetching its contents from JSON again and again when the user taps on my UISegmentedControl.
If you call method2 before method1 it will crash as copiedJasonArray has not been created. You should not create instance variables inside methods (as you cannot know if they have been called). You should do it when you create your viewController, in viewDidLoad for example.
And use properties:
#interface
#property (retain) NSMutableArray* copiedJsonArray;
#end
then either
#synthesize copiedJsonArray = _copiedJsonArray
or leave that line it out (the compiler will put it in automatically in 4.5)
access as self.copiedJsonArray or _copiedJSONArray.
Outside of getters,setters,inits and deallocs, use the self. form, it's safer.
You could also create _copiedJsonArray lazily in the setter:
- (NSMutableArray*) copiedJsonArray
{
if (!_copiedJasonArray)
_copiedJsonArray = [NSMutableArray alloc] init;
return _copiedJasonArray;
}
I am using NSURLConnection to fetch XML data from the server. I am parsing the data and showing it in a tableview. It all works as expected. Now, I would like to save downloaded data for an offline use. The idea was to take downloaded NSData, convert it to NSArray and store it either to NSUserDefaults or in a separate file. However, I am having problems converting NSData to NSArray.
I added the logic to (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data method. What I am trying to do is as follows:
NSError *error;
NSPropertyListFormat plistFormat;
id object = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:&plistFormat error:&error];
if (error != nil) {
NSLog(#"Error %#", [error localizedDescription]);
[error release];
}
if ([object isKindOfClass:[NSArray class]]) {
NSLog(#"IS Array");
NSArray *objectArray = object;
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:objectArray] forKey:#"myKey"];
} else {
NSLog(#"Not an array");
}
In log I get as follows:
Error The operation couldn’t be completed. (Cocoa error 3840.)
Not an
array
If I remove error handling and leave just the line
NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithData:data];
my application crashes with the following message: `
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '*** -[NSKeyedUnarchiver
initForReadingWithData:]: incomprehensible archive (0x3c, 0x3f, 0x78,
0x6d, 0x6c, 0x20, 0x76, 0x65)'
Why is this happening? What is Cocoa error 3840?
My object implements NSCoding protocol, and has methods encodeWithCoder and initWithCoder. Does every property of my object has to be encoded / decoded?
Edit: Here is my object:
Currency.h
#interface Currency : NSObject<NSCoding>{
CGFloat value;
NSString *code;
NSDate *date;
NSString *description;
NSString *imagePath;
}
#property (nonatomic, assign) CGFloat value;
#property (nonatomic, retain) NSString *code;
#property (nonatomic, retain) NSDate *date;
#property (nonatomic, retain) NSString *description;
#property (nonatomic, retain) NSString *imagePath;
Currency.m
#implementation Currency
#synthesize value;
#synthesize code;
#synthesize date;
#synthesize description;
#synthesize imagePath;
static NSString * const keyCode = #"code";
static NSString * const keyDescription = #"description";
static NSString * const keyValue = #"value";
- (void)dealloc {
[code release];
[date release];
[description release];
[imagePath release];
[super dealloc];
}
- (void)encodeWithCoder:(NSCoder *)coder
{
if ([coder allowsKeyedCoding]) {
[coder encodeObject:code forKey: keyCode];
[coder encodeObject:description forKey: keyDescription];
[coder encodeFloat:value forKey: keyValue];
}
}
}
- (id)initWithCoder:(NSCoder *) coder {
self = [[Currency alloc] init];
if (self != nil)
{
code = [[coder decodeObjectForKey:keyCode] retain];
description = [[coder decodeObjectForKey:keyDescription] retain];
value = [coder decodeFloatForKey:keyValue];
}
return self;
}
#end
Revised answer:
So, there's three different concepts here: Generic XML, propertyLists (in either binary or XML format), and an Archive. You parse XML with an NSXMLParser or other library to convert into Obj-C objects. You can save core Obj-C objects (NOT including your custom class Currency) into property lists and read them back. Or you can archive any object/object graph (using encoders) into an archive and read it back with NSKeyedUnarchiver.
Even though underneath they may share implementations, you can't mix them. For example, first you tried to read XML as a propertyList, and even though pLists are XML, your generic CurrencyData XML doesn't have the right format for a plist (e.g. no ). In addition, even if you were to write a plist out, you'd have to convert Currency to a NSDictionary in order to make it storable in a plist, which means you wouldn't need the encoders.
Then you tried to read XML with NSKeyedUnarchiver and got the message "incomprehensible archive". Also correct, as it wasn't created with NSKeyedArchive.
So you can't use the original stream and directly plist or unarchive into your objects. But to save the parsed XMLArray for offline use, you can either convert Currency into an NSDictionary and use property lists, or just leave it as it is and use NSKeyedArchive and Unarchive like this (which leverages the encoders/decoders you've provided):
//To save
NSData * data = [NSKeyedArchiver archivedDataWithRootObject:xmlArray];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:#"myKey"]; //or store in another file
//To later read
NSData * data2 = [[NSUserDefaults standardUserDefaults] valueForKey:#"myKey"];
NSArray * newXMLArray = [NSKeyedUnarchiver unarchiveObjectWithData:data2];
Hope that clears it up. Investigating it also cleared up my own understanding of when to use which technique.
//Original comment:
If you've already parsed the incoming data into a model for your tableView, why not serialize that copy of the data rather than the original stream?
Is there a reason why you don't just save the NSData object directly to a file with
data writeToFile:(NSString*)path atomically:(BOOL)flag
?
I don't think you get an error in your NSPropertyList call - the propertyListWithData function returns nil if an error occurred, and I suppose the error is valid only then. You probably should check on return value != nil, and then print the error. Error 3840 marks the beginning of the range of property list errors, that doesn't look like an error in itself.
The description of propertyListWithData also says that it returns a property list, not an array, so it's not surprising that isKindOfClass says it's not an array ... see here:
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSPropertyListSerialization_Class/Reference/Reference.html
I've used this tutorial to create an app with a table view that is populated using an NSMutableArray. Now I'd like to add the functionality to add additional items to the array and save/load them. I've customized the Fruit class to look like this:
#import <UIKit/UIKit.h>
#interface Fruit : NSObject {
NSString *name;
NSString *instructions;
NSString *explination;
NSString *imagePath;
}
#property(nonatomic,copy) NSString *name;
#property(nonatomic,copy) NSString *instructions;
#property(nonatomic,copy) NSString *explination;
#property(nonatomic,copy) NSString *imagePath;
- (id)initWithName:(NSString*)n instructions:(NSString *)inst explination:(NSString *)why imagePath:(NSString *)img;
#end
and the Fruit.m file:
#import "Fruit.h"
#implementation Fruit
#synthesize name,instructions,explination,imagePath;
- (id)initWithName: (NSString*)n instructions:(NSString*)inst explination:(NSString *)why imagePath:(NSString *)img {
self.name = n;
self.instructions = inst;
self.explination = why;
self.imagePath = img;
return self;
}
#end
and this works great, I can load two textviews and an imageView, instead of just one textview. But how would I go about saving any new items the user creates, and loading them (if they exist) when the app gets launched again?
to save your array to disk you need a couple of things.
first you need to add some methods to your fruit class so it conforms to the NSCoding protocol.
The first method is - (id)initWithCoder:(NSCoder *)aDecoder. This method will be called when you create a Fruit object from a saved archive.
Second method is - (void)encodeWithCoder:(NSCoder *)aCoder. This method is used to save your fruit in an archive.
Sounds complicated? Actually it isn't. Just a couple lines of easy to understand code.
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
self.name = [aDecoder decodeObjectForKey:#"name"];
self.instructions = [aDecoder decodeObjectForKey:#"instructions"];
self.explanation = [aDecoder decodeObjectForKey:#"explanation"];
self.imagePath = [aDecoder decodeObjectForKey:#"imagePath"];
}
return self;
}
Look at first two lines of this init method. You have to call [super init] and do a check if self is not nil in your initWithName:instructions:explination:imagePath: method too. It won't change anything in this special case, but this will definitely change in the next few classes you write. So use it all the time.
I changed this for you. And I changed the spelling error.
- (id)initWithName: (NSString*)n instructions:(NSString*)inst explination:(NSString *)why imagePath:(NSString *)img {
self = [super init];
if (self) {
self.name = n;
self.instructions = inst;
self.explanation = why;
self.imagePath = img;
}
return self;
}
and the method for encoding:
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:name forKey:#"name"];
[aCoder encodeObject:instructions forKey:#"instructions"];
[aCoder encodeObject:explanation forKey:#"explanation"];
[aCoder encodeObject:imagePath forKey:#"imagePath"];
}
It's not necessary that the key name matches the variable name. You don't need to do this. But in my opinion it adds some clarity. As long as you decode with the same name you've used for encoding you can use whatever you want.
First part is done. Next you need to load and save your NSMutableArray to a file. But to do this you need the path to the documents directory. So I created a little helper method that goes into your controller.
- (NSString *)applicationDocumentsPath {
return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
}
Then we need to load the array from disk.
NSString *path = [[self applicationDocumentsPath] stringByAppendingPathComponent:#"some.fruits"];
NSMutableArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
if (!array) {
// if it couldn't be loaded from disk create a new one
array = [NSMutableArray array];
}
then you add as much fruits as you like, and finally, to save your array to disk you need this line.
BOOL result = [NSKeyedArchiver archiveRootObject:array toFile:path];
you can check result if the archive was done without error.
I guess this should get you started. Happy coding.
You would need to persist your array on the disk and load it when the app launches.
See the answer to this question.
Than you should read about archiving : NSArchiver
You would need to implement 2 method for your Fruit class :
EncodeWithEncoder and InitWithEncoder.
than you could archive you fruits array.
Good Luck.
Can anybody tell me how to make sure an NSArray exist in memory as long as the app is running?
Thanks......
You can retain the object in application delegate class and on application terminate release.
i.e
in application delegate class
#interface MyAppDelegate : NSObject <UIApplicationDelegate>
{
UIWindow *window;
NSMutableArray *arrayObjects;
}
#property (nonatomic, retain) IBOutlet UIWindow *window;
#property (nonatomic, retain) NSMutableArray *arrayObjects;
Now you can allocate the arrayObjects using the instance of delegate class and you can also use the value stored in arrays.
MyAppDelegate *appDelegate = (MyAppDelegate*)[[UIApplication sharedApplication]delegate];
appDelegate.arrayObjects = [[NSMutableArray alloc] initWithObjects:#"Object 1",#"Object 2",#"Object 3",nil];
This will preserve value in your array.Now You can use array any where in application after proper initialization.
If you're trying to preserve the array until the application exits, allocate it in your App Delegate's -init method and release it in the App Delegate's -dealloc method. Unless you make a mistake in your memory management and release the array too many times, it will be available for the entire lifecycle of the application.
For example:
#interface MyApp <NSApplicationDelegate>
{
NSArray *myArray;
}
#end
#implementation MyApp
- (id)init
{
if (nil != (self = [super init]))
{
myArray = [[NSArray alloc] init];
}
return self;
}
- (void)dealloc
{
[myArray release], myArray = nil;
[super dealloc];
}
#end
If I understand you correctly you want to store a NSArray instance to disk?
In that case use [NSKeyedArchiver archiveRootObject:myArray toFile:path]
The folder to store the file at can be determined with:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
I'm sure I'm missing something obvious here in a simple iPhone program I'm trying to write, but the code seems so basic that I can't see what's wrong.
I'm trying to use an NSMutableDictionary to store a list of classes and their associated save file names. In the header file I declare the dictionary
#interface ClassList : UITableViewController {
NSString *homedirectory;
NSString *masterindexpath;
NSMutableDictionary *classFilenameGlossary;
NSMutableArray *listofclasses;
}
#property (retain, nonatomic) NSString *homedirectory;
#property (retain, nonatomic) NSString *masterindexpath;
#property (retain, nonatomic) NSMutableDictionary *classFilenameGlossary;
#property (retain, nonatomic) NSMutableArray *listofclasses;
And, of course, in the implementation file:
#implementation ClassList
#synthesize homedirectory;
#synthesize masterindexpath;
#synthesize classFilenameGlossary;
#synthesize listofclasses;
I initialize this dictionary at ViewDidLoad from an existing file that saves the classlist:
- (void)viewDidLoad {
[super viewDidLoad];
// Get home directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
homedirectory = [paths objectAtIndex:0];
masterindexpath = [homedirectory stringByAppendingPathComponent:#"MasterIndex"];
// Get master course list or create it if necessary
NSFileManager *fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath:masterindexpath] == NO)
{
NSMutableDictionary *temporarydictionary = [NSMutableDictionary dictionary];
[temporarydictionary writeToFile:masterindexpath atomically:NO];
[temporarydictionary release];
}
[classFilenameGlossary initWithContentsOfFile:masterindexpath];
[homedirectory retain];
[masterindexpath retain];
}
Later, I add an item using setObject:forKey: but the dictionary never changes. Originally I was doing this in another view controller using a pointer back to this ClassList file, but once I discovered that didn't work, I simplified and tried to just set any sample object and key within the ClassList file:
[classFilenameGlossary setObject:#"sample filename" forKey:#"sample classname"];
I've looked at the debugger variable list and it shows classFilenameGlossary properly identified as an NSMutableDictionary with 0 key/value pairs. I'm not getting any sort of error; the dictionary simply never changes. At the moment, the dictionary loaded from the file masterindexpath is empty (since I haven't been able to add any items yet), but that shouldn't keep me from being able to add items to the dictionary.
I'm a total beginner at iPhone programming, so I'm sure there's something basic that I'm missing. I'd appreciate any help.
This line:
[classFilenameGlossary initWithContentsOfFile:masterindexpath];
should look like this:
classFilenameGlossary = [[NSMutableDictionary alloc] initWithContentsOfFile:masterindexpath];
You forgot to allocate memory for the NSMutableDictionary, so that's why it never initializes.