Cannot change mutable array? - objective-c

Ok so I have been stuck on this for a while even though it's a simple problem, I am trying to add an NSDictionary to an array however when calling the addObject method on the array the program crashes claiming I am sending a mutating method to an immutable object.
my code looks like:
- (IBAction)btnSaveMessage:(id)sender {
//Save The Message and Clear Text Fields
NSMutableDictionary *newMessageDictionary = [[NSMutableDictionary alloc] init];
[newMessageDictionary setObject:#"plist test title" forKey:#"Title"];
[newMessageDictionary setObject:#"plist subtitle" forKey:#"Subtitle"];
[newMessageDictionary setObject:#"-3.892119" forKey:#"Longitude"];
[newMessageDictionary setObject:#"54.191707" forKey:#"Lattitude"];
NSMutableArray *messagesArray =[[NSMutableArray alloc]init];
//Load Plist into array
NSString *messagesPath = [[NSBundle mainBundle] pathForResource:#"Messages"
ofType:#"plist"];
messagesArray = [NSArray arrayWithContentsOfFile:messagesPath];
[messagesArray addObject:newMessageDictionary]; //this causes crash
//write messagesarray to file
NSString *plistPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES) lastObject];
plistPath = [plistPath stringByAppendingPathComponent:#"usersMessages.plist"];
[messagesArray writeToFile:plistPath atomically:YES];
So I understand that I am trying to add to what the compiler see's as an immutable array but I declared it as Mutable?
Whats going on? :(

You are re-initializing messagesArray with
[NSArray arrayWithContentsOfFile:messagesPath]
Making it not mutable. Try:
[NSMutableArray arrayWithContentsOfFile:messagesPath];

You are overwriting your NSMutableArray with an NSArray in this line
messagesArray = [NSArray arrayWithContentsOfFile:messagesPath];
The class method arrayWithContentsOfFile: is returning just an NSArray and not an NSMutableArray.
If you want the content of the file mutable you can do this:
NSMutableArray *messagesArray = [[NSArray arrayWithContentsOfFile:messagesPath] mutableCopy];
and you can remove the previous declaration
NSMutableArray *messagesArray =[[NSMutableArray alloc]init];
now.

Related

NSMutableDictionary won't save data

hope someone can help me with a problem I've been wrestling with...
Using MapBox to develop a map-based app, and I want to attach an NSMutableDictionary to each of the map annotations to store additional data. I had it working but XCode kept throwing me warning about some of my data/object types, so I went through and tidied those up, and now it's broken. The idea is that on ViewDidLoad, the program runs through a set of plist dictionaries to set up each annotation correctly - that's still running okay, because my initial anno markers pop up with their correct settings. However rather than run back to the plist every time, I want to attach a dictionary to each annotation's userinfo property, which I can then use for toggling selection data and other functions. Here's my code:
NSDictionary *ExploreSteps = [[NSDictionary alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"ExploreSteps" ofType:#"plist"]];
for (NSString *key in [ExploreSteps allKeys])
{
//Loop through keys for each anno
NSDictionary *thisStep = [ExploreSteps objectForKey:key];
NSNumber *annoIndex = [thisStep objectForKey:#"Index"];
NSNumber *isLive = [thisStep valueForKey:#"isLive"];
NSString *annoTitle = [thisStep objectForKey:#"Title"];
NSString *annoText = [thisStep objectForKey:#"Text"];
NSString *imagefile = [thisStep objectForKey:#"Imagefile"];
double longitude = [[thisStep objectForKey:#"Longitude"] doubleValue];
double latitude = [[thisStep objectForKey:#"Latitude"] doubleValue];
NSString *pagefile = [thisStep objectForKey:#"Pagefile"];
NSString *audiofile = [thisStep objectForKey:#"Audiofile"];
CLLocationCoordinate2D annoCoord = CLLocationCoordinate2DMake(latitude, longitude);
RMAnnotation *annotation = [[RMAnnotation alloc] initWithMapView:mapView coordinate:annoCoord andTitle:annoTitle];
annotation.annotationIcon = [UIImage imageWithContentsOfFile: [[NSBundle mainBundle] pathForResource:imagefile ofType:#"png"]];
annotation.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:annoIndex, #"index", isLive, #"isLive", annoTitle, #"annoTitle", annoText, #"annoText", imagefile, #"imagefile", pagefile, #"pagefile", audiofile, #"audiofile", nil];
NSLog(#"Title: %#",[annotation.userInfo objectForKey:#"annoTitle"]);
[mapView addAnnotation:annotation];
}
The NSLog should spit out the annoTitle string, but instead it's giving me a null every time, and the behaviour of the rest of the app also shows that info stored in the dictionary simply isn't "getting through".
Any ideas? Thanks in advance!
ETA: Modified code for initializing the dictionary (not that it seems to make any difference to the problem!):
NSMutableDictionary *myUserInfo = [[NSMutableDictionary alloc] initWithObjectsAndKeys:annoIndex, #"index", isLive, #"isLive", annoTitle, #"annoTitle", annoText, #"annoText", imagefile, #"imagefile", pagefile, #"pagefile", audiofile, #"audiofile", nil];
annotation.userInfo = myUserInfo;
NSLog(#"Title: %#",[annotation.userInfo objectForKey:#"annoTitle"]);
NSLog(#"Length: %u",[[annotation.userInfo allKeys] count]);
(Title now returns "(null)", while Length returns "1", if that's at all helpful...)
Almost certainly one of your objects is nil. You mention that allKeys] count] returns 1 so I can go further and say that your value for isLive is nil. Hence your original line:
[NSMutableDictionary dictionaryWithObjectsAndKeys:annoIndex, #"index", isLive, #"isLive", annoTitle, #"annoTitle", annoText, #"annoText", imagefile, #"imagefile", pagefile, #"pagefile", audiofile, #"audiofile", nil];
Acts exactly the same as:
[NSMutableDictionary dictionaryWithObjectsAndKeys:annoIndex, #"index", nil, #"isLive", annoTitle, #"annoTitle", annoText, #"annoText", imagefile, #"imagefile", pagefile, #"pagefile", audiofile, #"audiofile", nil];
And the dictionary takes annoIndex to be the final key-value pair.
I'd suggest that probably you want to take a mutable copy of thisStep and strip out the keys you don't want, then pass it along as the userInfo.
It's the way you are creating the NSMutableDictionary for userInfo. Take a look at this Difference between [[NSMutableDictionary alloc] initWithObjects:...] and [NSMutableDictionary dictionaryWithObjects:...]?
"
+dictionaryWithObjects: returns an autoreleased dictionary
-initWithObjects: you must release yourself
if you want the dictionary to persist as a instance variable, you should create it with an init method or retain an autoreleased version, either way you should be sure to release it in your dealloc method
"

How to write data in .plist?

I'm trying to save some comments in a plist, that's OK cause its just a prototype. The problem is that i can read from plist but when I try to write and read after that, it throws an "array out of bounds" exception. I can't figure it out what I'm doing wrong here.
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"Comments" ofType:#"plist"];
NSMutableArray *plistArray = [[NSMutableArray alloc] initWithContentsOfFile:filePath];
NSMutableDictionary *newComment = [NSMutableDictionary dictionary];
[newComment setValue:commentTitle.text forKey:#"title"];
[newComment setValue:comment forKey:#"comment"];
[plistArray addObject:newComment];
[plistArray writeToFile:filePath atomically:NO];
That works fine, then i try to read:
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"Comments" ofType:#"plist"];
NSMutableArray *plistArray = [[NSMutableArray alloc] initWithContentsOfFile:filePath];
NSMutableDictionary *dictionary = (NSMutableDictionary *) [plistArray objectAtIndex:0];
NSLog(#"%#", [dictionary objectForKey:#"title"]);
And it throws the exception.
If I add the item manually to the plist, it works fine, i guess it means that my reading code its fine.
Could it be the structure of my plist?
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
</array>
</plist>
* Terminating app due to uncaught exception 'NSRangeException', reason: '-[__NSCFArray objectAtIndex:]: index (1) beyond bounds (1)'
I added the "description" to the array before writing to the plist. If i use the following code:
NSString *aDocumentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
// NSString *aFilePath = [NSString stringWithFormat:#"%#/Comments.plist", aDocumentsDirectory];
//
// NSMutableArray *plistArray = [[NSMutableArray alloc] initWithContentsOfFile:aFilePath];
The return is (null)
But if i use:
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"Comments" ofType:#"plist"];
NSMutableArray *plistArray = [[NSMutableArray alloc] initWithContentsOfFile:filePath];
i can see the contents of the array, and its all working properly.
The problem is: In both ways i cant write to the file, it keeps returning "NO". And i already checked the permissions
You are trying to write the file into mainBundle. Definitely not possible.
You will have to write the plist file to Documents or Application Support folder of the app.
Create File Path in Documents Directory :
NSString *aDocumentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *aFilePath = [NSString stringWithFormat:#"%#/Comments.plist", aDocumentsDirectory];
Write to FilePath
[plistArray writeToFile:aFilePath atomically:YES];
Read From FilePath
NSMutableArray *plistArray = [[NSMutableArray alloc] initWithContentsOfFile:aFilePath];
I see two problems with your code:
(May or may not be a problem). If the file does not exist initially, the initWithContentsOfFile: selector will return nil, causing the rest of your code to be no-ops.
(Probably the cause). You may not write to the bundle resources directory. Store your file in the Documents or Caches directory instead.
To locate your documents directory, use something like this:
- (NSString*) pathForDocument:(NSString*)documentName {
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
if(documentDirectories.count < 1) return nil;
return [[documentDirectories objectAtIndex:0] stringByAppendingPathComponent:documentName];
}
First of all, why are you writing a file into your bundle?
Then, to address your problem, check if you actually did write the file.
if ([plistArray writeToFile:filePath atomically:NO])
NSLog (#"Written");
else
NSLog (#"Not Written");
Also, log your array when you're read it using -(void)description to check the contents of the dictionary.
Edit
As you said that you're not writing to your plist. For now, just create a test plist on your desktop.
NSString *testPath = [[NSString stringWithString:#"~/Desktop/Comments.plist"] stringByExpandingTildeInPath];
if ([plistArray writeToFile:testPath atomically:NO])
NSLog (#"Written");
else
NSLog (#"Not Written");
If that still returns Not Written, then there's something wrong with your dictionary. Which I doubt because it's just strings (Though they could be placeholders for asking your question on stackoverflow. The docs states that the classes in the dictionary must be of NSData, NSDate, NSNumber, NSString, NSArray, or NSDictionary). If that says written though, I'm guessing it doesn't write to your bundle because of permissions, which then you have to change your plist location to somewhere else other than your bundle, which I highly recommend.
If you only put one item in the array, you should obviously use index 0 instead of 1 when reading from it:
NSMutableDictionary *dictionary = (NSMutableDictionary *) [plistArray objectAtIndex:0];

Deep mutable copy of a NSDictionary into a NSMutableDictionary

I have a property list which I read into an NSDictionary, from which I would like to create a mutable copy into an NSMutableDictionary to edit the contents and - later be able to reset the mutable dictionary by copying back the original contents.
NSDictionary *defaultRows;
NSMutableDictionary *rows;
NSString *plistPath = [[NSBundle mainBundle] pathForResource:#"Config" ofType:#"plist"];
defaultRows = [[[NSDictionary alloc] initWithContentsOfFile: plistPath] objectForKey:#"rows"];
This is how I try to create a copy of the original dictionary (according to Apple's recommendation):
rows = [NSMutableDictionary dictionaryWithDictionary: defaultRows];
When I try to change the contents of the dictionary rows with:
[[[rows objectForKey:self.company.coaTypeCode] objectForKey:statementType] removeObjectsAtIndexes:rowsToDelete];
I get the runtime error *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-[__NSCFArray removeObjectAtIndex:]: mutating method sent to immutable object'.
I have read the article here, but wonder if there is a more elegant method to implement a deep copy?
Since you are loading defaultRows from a property list, the easiest way to do this is to just deserialize the property list with mutable containers.
NSString *plistPath = [[NSBundle mainBundle] pathForResource:#"Config" ofType:#"plist"];
NSData *rowsData = [NSData dataWithContentsOfFile:plistPath];
NSMutableDictionary *rows = [NSPropertyListSerialization propertyListWithData:rowsData
options:NSPropertyListMutableContainers format:NULL error:NULL];
Did you consider using
NSMutableDictionary *rows = [[[NSMutableDictionary alloc] initWithContentsOfFile:plistPath] objectForKey:#"rows"];

Change from a NSMutableArray to a .plist

I have been working through several tutorials on uitableviews.
I have put, as instructed, all the info into a 'listofitems' as below
listOfItems = [[NSMutableArray alloc] init];
NSArray *countriesToLiveInArray = [NSArray arrayWithObjects:#"Iceland", #"Greenland", #"Switzerland", #"Norway", #"New Zealand", #"Greece", #"Rome", #"Ireland", nil];
NSDictionary *countriesToLiveInDict = [NSDictionary dictionaryWithObject:countriesToLiveInArray forKey:#"Countries"];
NSArray *countriesLivedInArray = [NSArray arrayWithObjects:#"India", #"U.S.A", nil];
NSDictionary *countriesLivedInDict = [NSDictionary dictionaryWithObject:countriesLivedInArray forKey:#"Countries"];
[listOfItems addObject:countriesToLiveInDict];
[listOfItems addObject:countriesLivedInDict];
This creates a sectioned table view. I would like to know how to change it into a .plist instead of typing it all out into the RootViewController.m. I would still like it to be in a sectioned tableview.
Is there a simple method for changing from this NSMutableArray,NSArray and NSDictionary to a plist?
There's a simple method for this writeToFile:atomically::
[listOfItems writeToFile:destinationPath atomically:YES];
This will automatically create a file with plist inside it.
that sorta depends on what you want in a plist, and what you put into it. if the entries and contents are all CFPropertyList types (CFString,CFDate,CFData,CFDictionary,CFArray,CFNumber...) then just create it with something like CFPropertyListCreateDeepCopy.
if you have non-convertible custom objects (e.g., your own NSObject subclasses), then see the cocoa archiving topics.
This is the simple function end hear relization
This is function is updating NSArray
- (void) WriteRecordToFile:(NSMutableDictionary*)countDict {
// Here to write to file
databasePathCallCount = #"plist path";
NSMutableArray *countArray = [NSMutableArray arrayWithContentsOfFile:databasePathCallCount];
if(countArray)
[countArray addObject:countDict];
[countArray writeToFile:databasePathCallCount atomically:NO];
}

Plists and connections

I have an app that needs to connect and receive data, different each time that you click in one tab.
Then to show the data to the user, i use a "element.plist" where i have one array of dictionaries( each dictionary has the info in different strings: name, category, ...). I load the info from this plist.
I would like then, to continue using the same structure. Each time i receive the connection data:
delete the content in the plist
save the new content (I can do this in the parser method, each time that i have one object with all the information)
Read the info like i'm doing now.
The step that i can't do is the second.
thanks
I'm not sure I completely understand your question,
but I'll try to help.
below is some apple sample code that saves a plist when an application is exiting.
the second line sets the name of the plist file:
NSString *bundlePath = the application directory + "Data"
the third line defines a dictionary with all the data to be saves:
NSDictionary *plistDict
the fourth line formats this dictionary as property list data:
NSData *plistData
which then gets saved as Data.plist
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
NSString *errorDesc;
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:#"Data" ofType:#"plist"];
NSDictionary *plistDict = [NSDictionary dictionaryWithObjects:
[NSArray arrayWithObjects: personName, phoneNumbers, nil]
forKeys:[NSArray arrayWithObjects: #"Name", #"Phones", nil]];
NSData *plistData = [NSPropertyListSerialization dataFromPropertyList:plistDict
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorDesc];
if (plistData)
{
[plistData writeToFile:bundlePath atomically:YES];
}
else {
NSLog(errorDesc);
[errorDesc release];
}
return NSTerminateNow;
}
You can find this information in the Property list programming guide
Mey,
I'm not sure that I understand your statement about having an empty plist. I assume that you mean that if you read back the plist file that you created, it is null when you print it out. Suggesting that you are writing out an empty file or not reading correct or ...
I further assume that your intent is to replace the existing plist contents by a new plist while keeping the same name.
And caveat emptor - I'm new to Objective C etc. Here is a way to do that which I think you are trying to do.
// Implement viewDidLoad to do additional setup after loading the view,
// typically from a nib.
- (void)viewDidLoad {
NSBundle *bundle = [NSBundle mainBundle];
NSString *plistPath = [bundle pathForResource:#"TmpPList" ofType:#"plist"]; //Not NARC
//NSLog(#"plistPath : %#", plistPath);
//My plist is a simple array, but it could be an array of dictionary objects etc
NSMutableArray *arrayFromPList = [[NSMutableArray alloc] initWithContentsOfFile:plistPath]; //NARC
//NSLog(#"arrayFromPList : %#", arrayFromPList);
//Delete the arrays contents and put new contents
[arrayFromPList removeAllObjects];
//NSLog(#"arrayFromPList : %#", arrayFromPList);
//[arrayFromPList addObjectsFromArray:[NSArray arrayWithObjects:#"A", #"B", "#C", nil]];
//NSLog(#"arrayFromPList : %#", arrayFromPList);
[arrayFromPList setArray:[NSMutableArray arrayWithObjects:#"A", #"B", #"C", #"D", #"E", #"F", nil]];
//NSLog(#"arrayFromPList : %#", arrayFromPList);
/* */
//Write it out to the original file name
[arrayFromPList writeToFile:plistPath atomically:YES];
NSMutableArray *newArray = [[NSMutableArray alloc] initWithContentsOfFile:plistPath]; //NARC
NSLog(#"newArray : %#", newArray);
[arrayFromPList release];
[newArray release];
}