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.
Related
This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
Getting exception as “Collection was mutated while being enumerated”
This question is a continuation of another question I posted earlier How to read in plist data into a data model?
#devdavid's helped me to get this far...
I have a plist file called "HotelList.plist" and it looks like this:
<array>
<dict>
<key>hotelID</key>
<integer>0</integer>
<key>name</key>
<string>Solmar</string>
// ... more keys and strings ...
</dict>
// ... more hotel entries ...
</array>
I have a "hotel" class describing the keys.
I have a data model class where I would like to read in this plist into an array.
#import "DataModel.h"
#import "Hotel.h"
// Private methods
#interface DataModel ()
#property (nonatomic, retain) NSMutableArray *hotels;
-(void)loadHotels;
#end
#implementation DataModel
#synthesize hotels;
- (id)init {
if ((self = [super init])) {
[self loadHotels];
}
return self;
}
- (void)dealloc {
[hotels release];
[super dealloc];
}
- (void)loadHotels {
NSBundle* bundle = [NSBundle mainBundle];
NSString* plistpath = [bundle pathForResource:#"HotelList" ofType:#"plist"];
hotels = [[NSMutableArray arrayWithContentsOfFile:plistpath]retain];
for (NSDictionary *hotelDict in hotels) {
Hotel *hotel = [[Hotel alloc] init];
hotel.hotelID = [[hotelDict objectForKey:#"hotelID"] intValue];
hotel.name = [hotelDict objectForKey:#"name"];
[hotels addObject:hotel];
[hotel release];
}
}
#end
When I run this, the debugger shows me that each hotel dict was read in but when it reaches the end of the plist (I have about 30 hotels), it tries to go back to the first one and crashes, giving an exception "Collection was mutated while being enumerated".
The green SIGABRT indicator stops on the
for (NSDictionary *hotelDict in hotels) {
line. Is there something wrong with my for loop? The way I set up the arrays/dictionaries? Or maybe the formatting of the plist is wrong (although I don't think so because the debugger shows me it is reading it correctly)?
For completeness, I should mention that, yes, the plist file is present and is in the mainBundle, and spelled correctly. Also, the data in the plist is static -- I won't have to save anything new to a file.
Please help!
Use accessors, not ivars directly. Your problem would have been more obvious that way, and you'd avoid the possible leak you have in the assignment of hotel (if loadHotels is every called a second time). Your code reads dictionaries into an array and then tries to append Hotel objects onto that same array. Here's what you really meant to say:
NSArray *hotelDicts = [[NSArray alloc] initWithContentsOfFile:plistpath];
self.hotels = [NSMutableArray array];
for (NSDictionary *hotelDict in hotelDicts) {
Hotel *hotel = [[Hotel alloc] init];
hotel.hotelID = [[hotelDict objectForKey:#"hotelID"] intValue];
hotel.name = [hotelDict objectForKey:#"name"];
[self.hotels addObject:hotel];
[hotel release];
}
[hotelDicts release];
You're attempting to modify the collection you're enumerating through -- you can't do this. Instead of using an enumerator, try iterating through the collection instead. When you iterate through an array, you can still add/remove members of the array because you aren't bound to the enumeration that was defined prior to begining the for block.
One way to iterate through the collection is with a simple for loop:
for( int i = 0; i < [array count]; i++ )
{
id object = [array objectAtIndex:i];
// do something with object
}
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.
}
I have a model class that keeps track record being built by multiple views. It has a NSMutableDictionary that has the fields and values I eventually write to the database. It is saved to a plist and loaded back when needed. I thought that I was keeping track of my memory, but it throws a EXC_BAD_ACCESS when I try to release the Dictionary. Here is my interface:
#import <Foundation/Foundation.h>
#interface CurrentEntryModel : NSObject {
NSMutableDictionary *currentEntry;
}
#property (nonatomic, retain) NSMutableDictionary *currentEntry;
- (void) setValue: (NSString *)value;
- (NSString *) getValue;
#end
My understanding is that currentEntry should be retained and I would have to release it during dealloc.
Here is my implementation (this isn't the entire class just the relevant parts):
#import "CurrentEntryModel.h"
#implementation CurrentEntryModel
#synthesize currentEntry;
-(id) init {
if ( self = [super init] )
{
//check for file
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *file;
file = #"location.plist";
if ([fileManager fileExistsAtPath:file]){
NSLog(#"file exists");
currentEntry = [[NSMutableDictionary alloc] initWithContentsOfFile:file];
}else {
NSLog(#"file doesn't exist");
currentEntry = [[NSMutableDictionary alloc ] initWithCapacity:1];
NSDate *testDate = [NSDate date];
[currentEntry setObject:testDate forKey:#"created"];
[currentEntry writeToFile:file atomically:YES];
}
}
return self;
}
- (void) setValue: (NSString *)value {
[currentEntry setObject:value forKey:#"location"];
}
- (NSString *) getValue {
return [currentEntry objectForKey:#"location"];
}
- (void) dealloc{
[currentEntry release];
[super dealloc];
}
#end
If I init this class it will automatically create the dictionary and if I call one of the set or get methods it seems like the dictionary is retained as it will dealloc correctly. If the class is just initialized and then no methods are called it will throw the EXC_BAD_ACCESS errors. If I am not mistaken when the file doesn't exist I don't initialize the dictionary correctly because the method starts with dictionary and not init. Although every time I run this the file is there so it always uses the the file found logic and I thought that that will retain the variable.
Am I not initializing the dictionary correctly?
Edit - changed the code on the convenience method to reflect the proper way. Everyone take note of what Squeegy has to say.
This is bad bad bad.
else {
NSLog(#"file doesn't exist");
currentEntry = [[NSMutableDictionary alloc ] dictionaryWithCapacity:1];
dictionaryWithCapacity: is a class method on NSMutableDictionary which returns an autoreleased object, and you don't retain it. So the run loop ends, and the dictionary gets autoreleased. Then you run [currentEntry release] in your dealloc and it explodes because that object is deallocated already.
you probably wan't initWithCapacity: instead. Always pair alloc with a method that starts with init.
Also, when using retained properties like this, I usually let the property figure this out for me, and only work with autoreleased objects. You just have to remember less rules, and there are less gotchas.
- (id)init {
// ...
self.currentEntry = [NSMutableDictionary dictionWithContentsOfFile:file];
// ...
}
- (void)dealloc {
//...
self.currentEntry = nil;
//...
}
This way you never have to call retain or release directly on the object. In my experience, this results in less confusing bugs. But it's also point of style among many ObjC programmer that not everyone agrees with.
Joshua -
+ (id)dictionaryWithCapacity:(NSUInteger)numItems
is a class method of NSDictionary. So when you call it, it should be:
[NSMutableDictionary dictionaryWithCapacity:1];
Not:
[[NSMutableDictionary alloc] dictionaryWithCapacity:1];
Further, [NSMutableDictionary dictionaryWithCapacity:] returns an autoreleased object. If you want to keep the dictionary as an ivar and not have it autoreleased on the next cycle of the run loop, you should call:
[currentEntry retain];
So, basically, change it to:
currentEntry = [[NSMutableDictionary alloc] initWithCapacity:1];
or:
currentEntry = [[NSMutableDictionary dictionaryWithCapacity:1] retain];
The first one probably makes more sense, since the connivence class methods were designed to be used when you wanted an autoreleased instance.
I have numerous classes that use the various NSDictionary/NSArray collection classes as ivars but often I run into the problem of my collection class getting released before the containing class is released.
This seems to happen mostly with the collections classes and not with another model class (ie classes that I either created separately or other NS* non-collection classes).
Here are the two variations I've done and seen other people do:
#implementation ClassX
// myDictionary declared as a property in the .h file as this:
// #property (nonatomic, retain) NSMutableDictionary *myDictionary;
#synthesize myDictionary;
- (id)int
{
if (self = [super init])
{
// Option 1:
// If I don't instantiate and assign with 'self',
// myDictionary ivar will not be available
// at times in doSomething.
myDictionary = [NSMutableDictionary dictionary];
// Option 2:
// Doing this, however will keep the dictionary around.
// because I have invoked an extra retain on the dictionary
self.myDictionary = [NSMutableDictionary dictionary];
// Which one is more correct?
}
return self;
}
- (void)doSomething
{
// this will give the error about trying to invoke
// a method on an already released instance
[myDictionary objectForKey:#"myKey"];
}
- (void)dealloc
{
// If I did self.myDictionary in 'init', I then
// need to do this:
[myDictionary release];
[super dealloc];
}
#end
So which approach is the more correct way to hold an instance of NSDictionary within a class?
Option 2 is correct; Option 1 is wrong.
But you left out the best option: myDictionary = [[NSMutableDictionary alloc] init].
I recommend using
myDictionary = [[NSMutableDictionary alloc] init];
The memory is only within the scope of the method you're in if you call [NSMutableDictionary dictionary]. Once you leave the method, that memory goes with it which is why you need to alloc/init if you want to retain the values.
That's why you don't have to release if you don't encounter an alloc.
So for instance:
- (void) doSomething {
// Do not need to release this string
NSString *someText = #"Hello world!";
// You need to release this string:
NSString *otherText = [[NSString alloc] initWithString:#"Hello world!"];
[otherText release];
}
Edited: Removed self after #mipadi #st3fan and caught my mistake. Forgot to post the change. Thanks for keeping me accountable.
I'm facing a strange problem with NSUsrDefaults. Whenever I'm fetching the data from NSUserDefaults, it's getting populated temporarily. I'm fetching it into viewDidLoad where it's fetched.
-(void)viewDidLoad{
companies = [NSMutableArray array];
oldCompanies = [[NSUserDefaults standardUserDefaults] arrayForKey:#"companyData"];
if( companies )
{
for( NSData *data in oldCompanies )
{
companyObj = (Company*) [NSKeyedUnarchiver unarchiveObjectWithData:data];
[companies addObject:companyObj];
}
}
}
But outside viewDidLoad, whenever I try to access the data, the array "oldCompanies" as well as "companies" are shown "nil".
EDIT:
I'm encoding my Company object in a class which subclasses NSCoding like shown below but not allocating or retaining the properties anywhere. Can this be the catch?
- (void)encodeWithCoder:(NSCoder *)encoder
{
//Encode properties, other class variables, etc
[encoder encodeObject:self.companyId forKey:#"id"];
[encoder encodeObject:self.companyTitle forKey:#"title"];
[encoder encodeObject:self.companyImage forKey:#"image"];
}
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if( self != nil )
{
//decode properties, other class vars
self.companyId = [decoder decodeObjectForKey:#"id"];
self.companyTitle = [decoder decodeObjectForKey:#"title"];
self.companyImage = [decoder decodeObjectForKey:#"image"];
}
return self;
}
Can anybody please help?
Thanx in advance.
+array creates an autoreleased array - if you want to take ownership of it per the memory management rules then you need to retain it:
[companies retain];
Or create it so that it isn't autoreleased:
companies = [[NSMutableArray alloc] init];
Or, better, let declared properties do that for you:
// interface, property declaration:
#property(retain, readwrite) NSMutableArray *companies;
// implementation:
#synthesize companies;
// ... using it:
self.companies = [NSMutableArray array];
You are not retaining the array, when you dont do an alloc, or a retain when instatiating an object you get an autoreleased object, in your example companies is autoreleased and is why you cant access it anymore at a later point you should either alloc it
[[NSMutableArray alloc] init] or retain it [NSMutableArray array] retain]...either way refer to memory managment guide to learn about objective-c memory managment memory managment ref
Are you trying to access the data before the view is loaded?
Objective-C doesn't reset your pointers for you. If the array isn't "persisted", then the pointer will point to garbage.