Memory buildup when parsing XML into a Core Data store using NSXMLParser - objective-c

I have a problem with an app that takes an XML feed, parses it, and stores the results into Core Data.
The problem only occurs on the very first run of the app when there is nothing in the store and the whole feed is parsed and stored.
The problem is simply that memory allocations build up and up until, on 50% of attempts it crashes the app, usually at around 10Mb.
The objects allocated seem to be CFData(store) objects and I can't seem to find any way to force a release of them.
If you can get it to run once and successfully complete the parsing and save to core data then every subsequent launch is fine, memory usage never exceeds 2.5Mb
Here's the general approach I have before we get into code:
Get the feed into an NSData object. Use NSFileManager to store it as a file.
Create a URL from the file path and give it to the parseXMLFile: method. Delegate is self.
On reaching parser:didStartElement:namespaceURI:qualifiedName:attributes: I create a dictionary to catch data from tags I need.
The parser:foundCharacters: method saves the contents of the tag to a string
The parser:didEndElement:... method then determines if the tag is something we need. If so it adds it to the dictionary if not it ignores it. Once it reaches the end of an item it calls a method to add it to the core data store.
From looking at sample code and other peoples postings here it seems there's nothing in the general approach thats wrong.
The code is below, it comes from a larger context of a view controller but I omitted anything not directly related.
In terms of things I have tried:
The feed is XML, no option to convert to JSON, sorry.
It's not the images being found and stored in the shared documents area.
Clues as to what it might be:
This is the entry I get from Instruments on the largest most numerous things allocated (256k per item):
Object Address Category Creation Time Live Size Responsible
Library Responsible Caller
1 0xd710000 CFData
(store) 16024774912 • 262144 CFNetwork URLConnectionClient::clientDidReceiveData(_CFData
const*,
URLConnectionClient::ClientConnectionEventQueue*)
And here's the code, edited for anonymity of the content ;)
-(void)parserDidStartDocument:(NSXMLParser *)feedParser { }
-(void)parserDidEndDocument:(NSXMLParser *)feedParser { }
-(void)parser:(NSXMLParser *)feedParser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict
{
eachElement = elementName;
if ( [eachElement isEqualToString:#"item" ] )
{
//create a dictionary to store each item from the XML feed
self.currentItem = [[[NSMutableDictionary alloc] init] autorelease];
}
}
-(void)parser:(NSXMLParser *)feedParser foundCharacters:(NSString *)feedString
{
//Make sure that tag content doesn't line break unnecessarily
if (self.currentString == nil)
{
self.currentString = [[[NSMutableString alloc] init]autorelease];
}
[self.currentString appendString:feedString];
}
-(void)parser:(NSXMLParser *)feedParser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
eachElement = elementName;
NSString *tempString = [self.currentString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ( [eachElement isEqualToString:#"title"] )
{
//skip the intro title
if (![tempString isEqualToString:#"Andy Panda UK"])
{
//add item to di citonary output to console
[self.currentItem setValue:tempString forKey:eachElement];
}
}
if ( [eachElement isEqualToString:#"link"] )
{
//skip the intro link
if (![tempString isEqualToString:#"http://andypanda.co.uk/comic"])
{
//add item to dicitonary output to console
[self.currentItem setValue:tempString forKey:eachElement];
}
}
if ( [eachElement isEqualToString:#"pubDate"] )
{
//add item to dicitonary output to console
[self.currentItem setValue:tempString forKey:eachElement];
}
if ( [eachElement isEqualToString:#"description"] )
{
if ([tempString length] > 150)
{
//trims the string to just give us the link to the image file
NSRange firstPart = [tempString rangeOfString:#"src"];
NSString *firstString = [tempString substringFromIndex:firstPart.location+5];
NSString *secondString;
NSString *separatorString = #"\"";
NSScanner *aScanner = [NSScanner scannerWithString:firstString];
[aScanner scanUpToString:separatorString intoString:&secondString];
//trims the string further to give us just the credits
NSRange secondPart = [firstString rangeOfString:#"title="];
NSString *thirdString = [firstString substringFromIndex:secondPart.location+7];
thirdString = [thirdString substringToIndex:[thirdString length] - 12];
NSString *fourthString= [secondString substringFromIndex:35];
//add the items to the dictionary and output to console
[self.currentItem setValue:secondString forKey:#"imageURL"];
[self.currentItem setValue:thirdString forKey:#"credits"];
[self.currentItem setValue:fourthString forKey:#"imagePreviewURL"];
//safety sake set unneeded objects to nil before scope rules kick in to release them
firstString = nil;
secondString = nil;
thirdString = nil;
}
tempString = nil;
}
//close the feed and release all the little objects! Fly be free!
if ( [eachElement isEqualToString:#"item" ] )
{
//get the date sorted
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = #"EEE, dd MMM yyyy HH:mm:ss ZZZZ";
NSDate *pubDate = [formatter dateFromString:[currentItem valueForKey:#"pubDate"]];
[formatter release];
NSArray *fetchedArray = [[NSArray alloc] initWithArray:[fetchedResultsController fetchedObjects]];
//build the string to make the image
NSString *previewURL = [#"http://andypanda.co.uk/comic/comics-rss/" stringByAppendingString:[self.currentItem valueForKey:#"imagePreviewURL"]];
if ([fetchedArray count] == 0)
{
[self sendToCoreDataStoreWithDate:pubDate andPreview:previewURL];
}
else
{
int i, matches = 0;
for (i = 0; i < [fetchedArray count]; i++)
{
if ([[self.currentItem valueForKey:#"title"] isEqualToString:[[fetchedArray objectAtIndex:i] valueForKey:#"stripTitle"]])
{
matches++;
}
}
if (matches == 0)
{
[self sendToCoreDataStoreWithDate:pubDate andPreview:previewURL];
}
}
previewURL = nil;
[previewURL release];
[fetchedArray release];
}
self.currentString = nil;
}
- (void)sendToCoreDataStoreWithDate:(NSDate*)pubDate andPreview:(NSString*)previewURL {
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[fetchedResultsController fetchRequest] entity];
newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newManagedObject setValue:[NSDate date] forKey:#"timeStamp"];
[newManagedObject setValue:[[currentItem valueForKey:#"title"] description] forKey:#"stripTitle"];
[newManagedObject setValue:[[currentItem valueForKey:#"credits"] description] forKey:#"stripCredits"];
[newManagedObject setValue:[[currentItem valueForKey:#"imageURL"] description] forKey:#"stripImgURL"];
[newManagedObject setValue:[[currentItem valueForKey:#"link"] description] forKey:#"stripURL"];
[newManagedObject setValue:pubDate forKey:#"stripPubDate"];
[newManagedObject setValue:#"NO" forKey:#"stripBookmark"];
//**THE NEW SYSTEM**
NSString *destinationPath = [(AndyPadAppDelegate *)[[UIApplication sharedApplication] delegate] applicationDocumentsDirectory];
NSString *guidPreview = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *guidImage = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *dpPreview = [destinationPath stringByAppendingPathComponent:guidPreview];
NSString *dpImage = [destinationPath stringByAppendingPathComponent:guidImage];
NSData *previewD = [NSData dataWithContentsOfURL:[NSURL URLWithString:previewURL]];
NSData *imageD = [NSData dataWithContentsOfURL:[NSURL URLWithString:[currentItem valueForKey:#"imageURL"]]];
//NSError *error = nil;
[[NSFileManager defaultManager] createFileAtPath:dpPreview contents:previewD attributes:nil];
[[NSFileManager defaultManager] createFileAtPath:dpImage contents:imageD attributes:nil];
//TODO: BETTER ERROR HANDLING WHEN COMPLETED APP
[newManagedObject setValue:dpPreview forKey:#"stripLocalPreviewPath"];
[newManagedObject setValue:dpImage forKey:#"stripLocalImagePath"];
[newManagedObject release];
before++;
[self.currentItem removeAllObjects];
self.currentItem = nil;
[self.currentItem release];
[xmlParserPool drain];
self.xmlParserPool = [[NSAutoreleasePool alloc] init];
}
[EDIT]
#Rog
I tried playing with NSOperation but no luck so far, the same me,ory build up with the same items, all about 256k each. Here's the code:
- (void)sendToCoreDataStoreWithDate:(NSDate*)pubDate andPreview:(NSString*)previewURL
{
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[fetchedResultsController fetchRequest] entity];
newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newManagedObject setValue:[NSDate date] forKey:#"timeStamp"];
[newManagedObject setValue:[[currentItem valueForKey:#"title"] description] forKey:#"stripTitle"];
[newManagedObject setValue:[[currentItem valueForKey:#"credits"] description] forKey:#"stripCredits"];
[newManagedObject setValue:[[currentItem valueForKey:#"imageURL"] description] forKey:#"stripImgURL"];
[newManagedObject setValue:[[currentItem valueForKey:#"link"] description] forKey:#"stripURL"];
[newManagedObject setValue:pubDate forKey:#"stripPubDate"];
[newManagedObject setValue:#"NO" forKey:#"stripBookmark"];
NSString *destinationPath = [(AndyPadAppDelegate *)[[UIApplication sharedApplication] delegate] applicationDocumentsDirectory];
NSString *guidPreview = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *guidImage = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *dpPreview = [destinationPath stringByAppendingPathComponent:guidPreview];
NSString *dpImage = [destinationPath stringByAppendingPathComponent:guidImage];
//Create an array and send the contents off to be dispatched to an operation queue
NSArray *previewArray = [NSArray arrayWithObjects:dpPreview, previewURL, nil];
NSOperation *prevOp = [self taskWithData:previewArray];
[prevOp start];
//Create an array and send the contents off to be dispatched to an operation queue
NSArray *imageArray = [NSArray arrayWithObjects:dpImage, [currentItem valueForKey:#"imageURL"], nil];
NSOperation *imagOp = [self taskWithData:imageArray];
[imagOp start];
[newManagedObject setValue:dpPreview forKey:#"stripLocalPreviewPath"];
[newManagedObject setValue:dpImage forKey:#"stripLocalImagePath"];
//[newManagedObject release]; **recommended by stackoverflow answer
before++;
[self.currentItem removeAllObjects];
self.currentItem = nil;
[self.currentItem release];
[xmlParserPool drain];
self.xmlParserPool = [[NSAutoreleasePool alloc] init];
}
- (NSOperation*)taskWithData:(id)data
{
NSInvocationOperation* theOp = [[[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(threadedImageDownload:)
object:data] autorelease];
return theOp;
}
- (void)threadedImageDownload:(id)data
{
NSURL *urlFromArray1 = [NSURL URLWithString:[data objectAtIndex:1]];
NSString *stringFromArray0 = [NSString stringWithString:[data objectAtIndex:0]];
NSData *imageFile = [NSData dataWithContentsOfURL:urlFromArray1];
[[NSFileManager defaultManager] createFileAtPath:stringFromArray0
contents:imageFile
attributes:nil];
//-=====0jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjmn **OLEG**
}

You can disable the caching :
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
[sharedCache release];
Or clear it :
[[NSURLCache sharedURLCache] removeAllCachedResponses];
This should fix your problem.

Take a look at AQXMLParser. There is a example app that shows how to integrate it with Core Data on a background thread. It's a streaming parser so memory usage is minimal.
Set up a second MOC and fetched results controller for the UI and refetch when the parsing is complete to get the new data. (one option is to register for change notifications when the background MOC saves to merge between contexts)
For images, there are a number of third party categories on UIImageVew that support asynchronous downloads and placeholder images. So you would parse the image URL, store it in Core Data, then set the URL on the image view when that item is viewed. This way the user doesn't need to wait for all the images to be downloaded.

Your problem is here:
[newManagedObject release];
Get rid of it, and the crashes will be gone (you don't release NSManagedObjects, Coredata will handle that for you).
Cheers,
Rog

Related

To see more than pointers in an array (objective C)

If i enumerate an array i get
<myArray: 0x71b26b0>
<myArray: 0x71b2830>
<myArray: 0x71b2900>
I could take it that myData is behind the pointers listed, but if I wanted to explicitly see (log) the contents at each address, how to do that?
I have tried the &myData to no avail
--
for the benefit of uchuugaka:
-(void)loadObservedItems{
NSString *path = [self observationFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
NSData *data = [[NSData alloc] initWithContentsOfFile:path];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
myArray = [unarchiver decodeObjectForKey:#"ObserveKey"];
[unarchiver finishDecoding];
} else {
myArray = [[NSMutableArray alloc] initWithCapacity:10];
}
NSLog(#" %#",myArray);
}
Add to MyClass.m:
-(NSString*)description {
NSMutableDictionary* descDict = [NSMutableDictionary dictionary];
[descDict addObject:someField forKey:#"someField"]
[descDict addObject:anotherField forKey:#"anotherField"];
[descDict addObject:yetAnotherField forKey:#"yetAnotherField"];
return [descDict description];
}
Then just use NSLog(#"myObject is %#", myObject);. Just like the big guys.
Slightly more sophisticated is to (within the method) pre-pend your class name and the object address to the result string, but that's usually unnecessary for simple debugging.
But I think you can do that like this:
return [NSString stringWithFormat:#"%# : %#", [super description], [descDict description]];

save records from WCF service in CoreData

I am new to iOS development and CoreData too. I am calling a .Net WCF service for displaying data in a UITableViewcontroller in my app.I am saving this data in CoreData. When I add a new record on the server,I want it to get displayed in the UITableViewController as well as saved in CoreData.But this doesnt happen.I have to do a "Reset Contents and Settings" on the Simulator and then run the application again. When I do this,the app displays the latest records from the service.It also saves the new record in CoreData.I am using SUDZC for interacting with the wcf service.The code for calling the service,displaying data in UITableViewController and saving it to CoreData looks like this:
- (void)viewDidLoad
{
[super viewDidLoad];
self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController];
[my_table setDataSource:self];
[my_table setDelegate:self];
EDViPadDocSyncService *service = [[EDViPadDocSyncService alloc]init];
[service getAllCategories:self action:#selector(handleGetAllCategories:)];
}
-(void)handleGetAllCategories:(id)value
{
if([value isKindOfClass:[NSError class]])
{
NSLog(#"This is an error %#",value);
return;
}
if([value isKindOfClass:[SoapFault class]])
{
NSLog(#"this is a soap fault %#",value);
return;
}
NSMutableArray *result = (NSMutableArray*)value;
self.myData = [[NSMutableArray array] init];//array for storing 'category name'
self.catId = [[NSMutableArray array]init];//array for storing 'category ID'
self.myData=[self getCategories];
/*store data in Core Data - START*/
NSMutableArray *coreDataCategoryarray = [[NSMutableArray alloc]init];
NSManagedObjectContext *context = [self managedObjectContext];
Categories *newCategory;//this is the CoreData 'Category' object
for(int j=0;j<[result count];j++)
{
EDVCategory *edvCat = [[EDVCategory alloc]init];//this is the SUDZC 'Category' object
edvCat = [result objectAtIndex:j];
if ([self.catId count]>0) {
for (int i=0; i<[self.catId count]; i++) {
if ([edvCat categoryId] == [[self.catId objectAtIndex:i] integerValue]) {
checkFlag=TRUE;
}
}
}
if (checkFlag == FALSE) {
newCategory = [NSEntityDescription insertNewObjectForEntityForName:#"Categories" inManagedObjectContext:context];
[newCategory setCategoryId:[NSNumber numberWithInt:[edvCat categoryId]]];
[newCategory setCategoryName:edvCat.categoryName];
[newCategory setDocCount:[NSNumber numberWithInt:[edvCat docCount]]];
[newCategory setCategoryType:[NSNumber numberWithShort:[edvCat categoryType]]];
[newCategory setSubCategoryId:[NSNumber numberWithInt:[edvCat subCategoryId]]];
[coreDataCategoryarray addObject:newCategory];
}
}
/*store data in Core Data - END*/
NSError *error = nil;
if (![context save:&error])
{
[coreDataCategoryarray release];
}
else
{
//return [coreDataCategoryarray autorelease];
[coreDataCategoryarray autorelease];
}
self.myData=[self getCategories];
[my_table reloadData];
}
-(NSMutableArray *)getCategories
{
NSFetchRequest *request = [[[NSFetchRequest alloc] init]autorelease];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Categories" inManagedObjectContext:__managedObjectContext];
NSSortDescriptor *sortByName = [[[NSSortDescriptor alloc] initWithKey:#"categoryId" ascending:YES] autorelease];
[request setSortDescriptors:[NSArray arrayWithObject:sortByName]];
[request setEntity:entity];
entity = nil;
NSError *error = nil;
NSMutableArray *fetchResults = [[__managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
[request setReturnsObjectsAsFaults:NO];
NSManagedObject *aTabrss;
NSMutableArray *arForGetCategory=[[NSMutableArray alloc]init];
for (aTabrss in fetchResults){
[arForGetCategory addObject:[aTabrss valueForKey:#"categoryName"]];
[self.catId addObject:[aTabrss valueForKey:#"categoryId"]];
}
return (arForGetCategory);
}
What changes should I make in my code so that it reflects the latest data from the service and saves it to CoreData(sqlite) at the same time?
It seems like the class that you really need is NSUserDefaults:
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/nsuserdefaults_Class/Reference/Reference.html

Parsing a .csv file from a server with Objective-C

I have looked for an answer of a long time and still not found one so I thought I'd ask the question myself.
In my iPad app, I need to have the capability of parsing a .csv file in order to populate a table. I am using http://michael.stapelberg.de/cCSVParse to parse the csv files. However, I have only been successful in parsing local files. I have been trying to access a file from a server but am getting nowhere.
Here is my code to parse a local .csv file:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 1)
{
//UITextField *reply = [alertView textFieldAtIndex:buttonIndex];
NSString *fileName = input.text;
NSLog(#"fileName %#", fileName);
CSVParser *parser = [CSVParser new];
if ([fileName length] != 0)
{
NSString *pathAsString = [[NSBundle mainBundle]pathForResource:fileName ofType:#"csv"];
NSLog(#"%#", pathAsString);
if (pathAsString != nil)
{
[parser openFile:pathAsString];
NSMutableArray *csvContent = [parser parseFile];
NSLog(#"%#", csvContent);
[parser closeFile];
NSMutableArray *heading = [csvContent objectAtIndex:0];
[csvContent removeObjectAtIndex:0];
NSLog(#"%#", heading);
AppDelegate *ap = [AppDelegate sharedAppDelegate];
NSManagedObjectContext *context = [ap managedObjectContext];
NSString *currentHeader = [heading objectAtIndex:0];
NSString *currentValueInfo = [heading objectAtIndex:1];
NSManagedObject *newObject = [NSEntityDescription insertNewObjectForEntityForName:#"Field" inManagedObjectContext:context];
[newObject setValue:#"MIS" forKey:#"header"];
[newObject setValue:currentHeader forKey:#"fieldName"];
for (NSArray *current in csvContent)
{
NSManagedObject *newField = [NSEntityDescription insertNewObjectForEntityForName:#"Field" inManagedObjectContext:context];
[newField setValue:currentHeader forKey:#"header"];
[newField setValue:currentValueInfo forKey:#"valueInfo"];
NSLog(#"%#", [current objectAtIndex:0]);
[newField setValue:[current objectAtIndex:0] forKey:#"fieldName"];
[newField setValue:[NSNumber numberWithDouble:[[current objectAtIndex:1] doubleValue]] forKey:#"value"];
}
NSError *error;
if (![context save:&error])
{
NSLog(#"Couldn't save: %#", [error localizedDescription]);
}
[self storeArray];
[self.tableView reloadData];
}
}
}
input.text = nil;
}
Forgive the weird beginning and ending brace indentation. :/
Anyway, so that is my code to take input from a user and access a file locally which I'm sure you guys have realized already. Now I want to know how to get the path of a file in my server.
Also if you guys see anything else wrong such as writing style and other bad habits please tell me as I'm new to iOS.
Thank you so much in advance! If you didn't understand my question please clarify as I'm bad at explaining myself at times! :)
As I am guessing you are trying to get data from a server's .csv file and want to show that data in table view list.
so I suggest you try to get that .csv file data in NSData and then work on that.
NSData *responseData = [NSData dataWithContentsOfURL:[NSURL URLWithString:#"serverUrl"]];
NSString *csvResponseString = [[[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] autorelease];
NSLog(#"responseString--->%#",csvResponseString);
Now try to use nsstring's method (componentsSeparatedByString) with coma (')
arrSepratedData = [[responseString componentsSeparatedByString:#","];
Now use this arr for UITableView data populate.

How can I improve the XML parsing performance of my iOS code?

This may have been asked a lot but I'm still lost. I need to parse an XML file that I retrieve from Google Reader's API. Basically, it contains objects such as below :
<object>
<string name="id">feed/http://developer.apple.com/news/rss/news.rss</string>
<string name="title">Apple Developer News</string>
<list name="categories">
<object>
<string name="id">user/17999068807557229152/label/Apple</string>
<string name="label">Apple</string>
</object>
</list>
<string name="sortid">DB67AFC7</string>
<number name="firstitemmsec">1317836072018</number>
<string name="htmlUrl">http://developer.apple.com/news/</string>
</object>
I have tried with NSXMLParser and it works but it is really slow. Maybe my code is not the most efficient but still, it can take more than 10 second to parse and save an object into Core Data. I also have taken a look a several other libraries but their use seem a bit complicated and heavy for such a small XML file.
What do you think I should use ?
Thank you.
EDIT
Here the parser code:
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
if([elementName isEqualToString:#"list"] && [[attributeDict objectForKey:#"name"] isEqualToString:#"subscriptions"]){
subscriptionListFound = YES;
}
if(subscriptionListFound){
if([elementName isEqualToString:#"list"] && [[attributeDict objectForKey:#"name"] isEqualToString:#"categories"]){
categoryFound = YES;
currentCategoryId = [[[NSMutableString alloc] init] autorelease];
currentCategoryLabel = [[[NSMutableString alloc] init] autorelease];
}
if([elementName isEqualToString:#"object"] && !subscriptionFound && !categoryFound){
subscriptionFound = YES;
currentSubscriptionTitle = [[[NSMutableString alloc] init] autorelease];
currentSubscriptionId = [[[NSMutableString alloc] init] autorelease];
currentSubscriptionHtmlURL = [[[NSMutableString alloc] init] autorelease];
}
if([elementName isEqualToString:#"string"] && [[attributeDict objectForKey:#"name"] isEqualToString:#"id"]){
if(categoryFound){
categoryIdFound = YES;
}
else{
subscriptionIdFound = YES;
}
}
if([elementName isEqualToString:#"string"] && [[attributeDict objectForKey:#"name"] isEqualToString:#"title"]){
subscriptionTitleFound = YES;
}
if([elementName isEqualToString:#"string"] && [[attributeDict objectForKey:#"name"] isEqualToString:#"label"]){
categoryLabelFound = YES;
}
if([elementName isEqualToString:#"string"] && [[attributeDict objectForKey:#"name"] isEqualToString:#"htmlUrl"]){
subscriptionHtmlURLFound = YES;
}
}
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
if([elementName isEqualToString:#"list"] && !categoryFound){
subscriptionListFound = NO;
}
if([elementName isEqualToString:#"list"] && categoryFound){
categoryFound = NO;
}
if([elementName isEqualToString:#"object"] && !categoryFound && subscriptionFound){
[self saveSubscription];
[[NSNotificationCenter defaultCenter] postNotificationName:#"currentSubscriptionNotification" object:currentSubscriptionTitle];
subscriptionFound = NO;
}
if([elementName isEqualToString:#"string"]){
if(subscriptionIdFound == YES) {
[currentSubscriptionId appendString:self.currentParsedCharacterData];
subscriptionIdFound = NO;
}
if(subscriptionTitleFound == YES) {
[currentSubscriptionTitle appendString:self.currentParsedCharacterData];
subscriptionTitleFound = NO;
}
if(subscriptionHtmlURLFound == YES) {
[currentSubscriptionHtmlURL appendString:self.currentParsedCharacterData];
subscriptionHtmlURLFound = NO;
}
if(categoryIdFound == YES) {
[currentCategoryId appendString:self.currentParsedCharacterData];
categoryIdFound = NO;
}
if(categoryLabelFound == YES) {
[currentCategoryLabel appendString:self.currentParsedCharacterData];
categoryLabelFound = NO;
}
}
[self.currentParsedCharacterData setString:#""];
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
[self.currentParsedCharacterData appendString:string];
}
Here the code to save by means of CoreData:
- (void) saveSubscription {
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:
[NSEntityDescription entityForName:#"Group" inManagedObjectContext:context]];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat: #"(id == %#)",self.currentCategoryId]];
[fetchRequest setSortDescriptors: [NSArray arrayWithObject:
[[[NSSortDescriptor alloc] initWithKey: #"id"
ascending:YES] autorelease]]];
NSError *error2 = nil;
NSArray *foundGroups = [context executeFetchRequest:fetchRequest error:&error2];
if ([foundGroups count] > 0) {
self.currentGroupObject = [foundGroups objectAtIndex:0];
}
else {
self.currentGroupObject = [NSEntityDescription insertNewObjectForEntityForName:#"Group" inManagedObjectContext:context];
[self.currentGroupObject setId:self.currentCategoryId];
[self.currentGroupObject setLabel:self.currentCategoryLabel];
}
fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:
[NSEntityDescription entityForName:#"Subscription" inManagedObjectContext:context]];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat: #"(id == %#)", self.currentSubscriptionId]];
[fetchRequest setSortDescriptors: [NSArray arrayWithObject:
[[[NSSortDescriptor alloc] initWithKey: #"id"
ascending:YES] autorelease]]];
error2 = nil;
NSArray *foundSubscriptions = [context executeFetchRequest:fetchRequest error:&error2];
if ([foundSubscriptions count] > 0) {
self.currentSubscriptionObject = [foundSubscriptions objectAtIndex:0];
}
else {
self.currentSubscriptionObject = [NSEntityDescription insertNewObjectForEntityForName:#"Subscription" inManagedObjectContext:context];
[self.currentSubscriptionObject setId:self.currentSubscriptionId];
[self.currentSubscriptionObject setTitle:self.currentSubscriptionTitle];
[self.currentSubscriptionObject setHtmlURL:self.currentSubscriptionHtmlURL];
NSString *faviconURL = [self favIconUrlStringFromURL:self.currentSubscriptionHtmlURL];
NSString *faviconPath = [self saveFavicon:self.currentSubscriptionTitle url:faviconURL];
[self.currentSubscriptionObject setFaviconPath:faviconPath];
[self.currentSubscriptionObject setGroup:self.currentGroupObject];
[self.currentGroupObject addSubscriptionObject:self.currentSubscriptionObject];
}
NSError *error;
if (![context save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
}
Your parsing logic is quite inefficient - you are doing the same test over and over again by saying
if (string and x) do this
if (string and y) do this
if (string and z) do this
Instead of
if (string)
if (x) do this
if (y) do this
if (z) do this
All those unnecessary string comparisons are probably why your parsing is so slow. Same goes for all the object lookups. If you need a value multiple times, get it once and then store it in a variable.
Objective C method calls are relatively slow and can't be optimised away by the compiler, so if the value doesn't change you should call the method once and then store it.
So for example, this:
if([elementName isEqualToString:#"string"] && [[attributeDict objectForKey:#"name"] isEqualToString:#"id"]){
if(categoryFound){
categoryIdFound = YES;
}
else{
subscriptionIdFound = YES;
}
}
if([elementName isEqualToString:#"string"] && [[attributeDict objectForKey:#"name"] isEqualToString:#"title"]){
subscriptionTitleFound = YES;
}
if([elementName isEqualToString:#"string"] && [[attributeDict objectForKey:#"name"] isEqualToString:#"label"]){
categoryLabelFound = YES;
}
if([elementName isEqualToString:#"string"] && [[attributeDict objectForKey:#"name"] isEqualToString:#"htmlUrl"]){
subscriptionHtmlURLFound = YES;
}
Could be rewritten as this:
NSString *name = [attributeDict objectForKey:#"name"];
if([elementName isEqualToString:#"string"])
{
if ([name isEqualToString:#"id"])
{
if(categoryFound){
categoryIdFound = YES;
}
else{
subscriptionIdFound = YES;
}
}
else if ([name isEqualToString:#"title"])
{
subscriptionTitleFound = YES;
}
else if ([name isEqualToString:#"label"])
{
categoryLabelFound = YES;
}
else if ([name isEqualToString:#"htmlUrl"])
{
subscriptionHtmlURLFound = YES;
}
}
Which is way more efficient.
I suggest you to use GDataXML. It's quite simple to use and very fast. For further info you can read at how-to-read-and-write-xml-documents-with-gdataxml.
I've already replied to a similar question on how to read attribute with GDataXML in this Stack Overflow topic: get-xml-response-value-with-gdataxml.
I my opinion, the best library for parsing XML on iOS is TouchXML. It allows you to parse XML using xPaths and has advanced element parsing options. You can also parse XHTML documents with this.
Parsing is very easy:
NSData *xmlData = read your xml file
CXMLDocument *doc = [[CXMLDocument alloc] initWithData:xmlData options:0 error:nil]
NSArray *objects = [doc nodesForXPath:#"//object" error:nil];
for (CXMLElement *object in objects) {
NSArray *children = [object children];
for(CXMLElement *child in children) {
if([[child name] isEqualToString:#"string"]) {
// you are parsing <string> element.
// you can obtain element attribute by:
NSString *name = [[child attributeForName:#"name"] stringValue];
// you can obtain string between <></> tags via:
NSString *value = [child stringValue];
} else if([[child name] isEqualToString:#"list"]) {
// you are parsing <list> element.
} else if ...
}
}
After having developed a few apps with similar needs as yours, I would wholeheartedly recommend the AQToolkit
My usual setup for parsing XML is more or less like this:
Create a separate queue, using either GCD og NSOperationsQueue
Set up a input stream using HTTPMessage and AQGZipInputStream
Example Code:
HTTPMessage *message = [HTTPMessage requestMessageWithMethod:#"GET" url:url version:HTTPVersion1_1];
[message setUseGzipEncoding:YES];
AQGzipInputStream *inputstream = [[AQGzipInputStream alloc] initWithCompressedStream: [message inputStream]];
Hand the stream to a separate parser delegate, which creates a separate NSManagedObjectContext, and merges changes into main NSManagedObjectContext on save (NSManagedObject is not thread safe!)
Example code for initializing the context, and adding notifications for merging:
-(void)parserDidStartDocument:(AQXMLParser *)parser
{
self.ctx=[[NSManagedObjectContext alloc] init];
[self.ctx setMergePolicy: NSMergeByPropertyObjectTrumpMergePolicy];
[self.ctx setPersistentStoreCoordinator: [Database db].persistentStoreCoordinator];
NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
[dnc addObserver:self selector:#selector(mergeContextChanges:) name:NSManagedObjectContextDidSaveNotification object:self.ctx];
parsedElements = 0;
}
- (void)mergeContextChanges:(NSNotification *)notification{
SEL selector = #selector(mergeHelper:);
[self performSelectorOnMainThread:selector withObject:notification waitUntilDone:YES];
}
- (void)mergeHelper:(NSNotification*)saveNotification
{
// Fault in all updated objects
NSArray* updates = [[saveNotification.userInfo objectForKey:#"updated"] allObjects];
for (NSInteger i = [updates count]-1; i >= 0; i--)
{
[[[Database db].managedObjectContext objectWithID:[[updates objectAtIndex:i] objectID]] willAccessValueForKey:nil];
}
// Merge
[[Database db].managedObjectContext mergeChangesFromContextDidSaveNotification:saveNotification];
}
In my mind, choosing the right parser is more critical for huge datasets. If your dataset is manageable, then you have a lot to gain from a decent implementation. Using any libxml based parser, and parsing chunks of data as you receive them will give you significant performance increases from parsing data after it is downloaded.
Depending on your datasource, libz might throw Z_BUF_ERROR (at least in the simulator). I've suggested a solution in a pull-request on the AQToolkit, but I'm quite sure there would be even better solutions out there!

Method/IBAction stuck in infinite loop. Still no success

Now this may sound like my earlier problem/question but I've changed and tried a few things that were answered in my other questions to try to make it work, but I've still got the same problem.
I am observing a core data property from within a NSManagedObject sub-class and the method that gets called when the property changes calls another method but in this method it adds Core Data objects which triggers the KVO method which triggers the method again and so forth. Or so it seems, I'm not too sure about that because something different seems to happen, here is the series of events …
I click a button syncing with iCal (this in an IBAction with the exact same code thats in the method syncKVO). This sync works fine.
I add an object to my outline view. All is well.
I change its name which triggers the KVO Declaration (because I changed the 'name' property) which syncs with iCal. Works fine.
I delete the object I just added and somehow it triggers the KVO declaration (thus triggering the method) and puts me into an infinite loop.
Now for some code.
Code inside the NSManagedObject Subclass (called JGManagedObject) …
- (void) awakeFromFetch {
[self addObserver:[NSApp delegate] forKeyPath:#"name" options:0 context:nil];
}
- (void) awakeFromInsert {
[self addObserver:[NSApp delegate] forKeyPath:#"name" options:0 context:nil];
}
+ (void) addObserver{
[self addObserver:[NSApp delegate] forKeyPath:#"name" options:0 context:nil];
}
+ (void) removeObserver{
[self removeObserver:[NSApp delegate] forKeyPath:#"name"];
}
The KVO Declaration (inside the App Delegate) …
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:#"name"]) {
[self performSelector:#selector(syncKVO:)];
}
}
The Method (also inside the App Delegate)…
- (void)syncKVO:(id)sender {
NSManagedObjectContext *moc = [self managedObjectContext];
[syncButton setTitle:#"Syncing..."];
NSString *dateText = (#"Last Sync : %d", [NSDate date]);
[syncDate setStringValue:dateText];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:#"projects" inManagedObjectContext:moc];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
NSError *error = nil;
NSArray *array = [moc executeFetchRequest:request error:&error];
if (array == nil)
{
NSAlert *anAlert = [NSAlert alertWithError:error];
[anAlert runModal];
}
NSArray *namesArray = [array valueForKey:#"name"];
NSPredicate *predicate = [CalCalendarStore taskPredicateWithCalendars:[[CalCalendarStore defaultCalendarStore] calendars]];
NSArray *tasksNo = [[CalCalendarStore defaultCalendarStore] tasksWithPredicate:predicate];
NSArray *tasks = [tasksNo valueForKey:#"title"];
NSMutableArray *namesNewArray = [NSMutableArray arrayWithArray:namesArray];
[namesNewArray removeObjectsInArray:tasks];
NSLog(#"%d", [namesNewArray count]);
NSInteger *popIndex = [calenderPopup indexOfSelectedItem];
//Load the array
CalCalendarStore *store = [CalCalendarStore defaultCalendarStore];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString *supportDirectory = [paths objectAtIndex:0];
NSString *fileName = [supportDirectory stringByAppendingPathComponent:#"oldtasks.plist"];
NSMutableArray *oldTasks = [[NSMutableArray alloc] initWithContentsOfFile:fileName];
[oldTasks removeObjectsInArray:namesArray];
NSLog(#"%d",[oldTasks count]);
//Use the content
NSPredicate* taskPredicate = [CalCalendarStore taskPredicateWithCalendars:[[CalCalendarStore defaultCalendarStore] calendars]];
NSArray* allTasks = [[CalCalendarStore defaultCalendarStore] tasksWithPredicate:taskPredicate];
// Get the calendar
CalCalendar *calendar = [[store calendars] objectAtIndex:popIndex];
// Note: you can change which calendar you're adding to by changing the index or by
// using CalCalendarStore's -calendarWithUID: method
// Loop, adding tasks
for(NSString *title in namesNewArray) {
// Create task
CalTask *task = [CalTask task];
task.title = title;
task.calendar = calendar;
// Save task
if(![[CalCalendarStore defaultCalendarStore] saveTask:task error:&error]) {
NSLog(#"Error");
// Diagnostic error handling
NSAlert *anAlert = [NSAlert alertWithError:error];
[anAlert runModal];
}
}
NSMutableArray *tasksNewArray = [NSMutableArray arrayWithArray:tasks];
[tasksNewArray removeObjectsInArray:namesArray];
NSLog(#"%d", [tasksNewArray count]);
for(NSString *title in tasksNewArray) {
NSManagedObjectContext *moc = [self managedObjectContext];
JGManagedObject *theParent =
[NSEntityDescription insertNewObjectForEntityForName:#"projects"
inManagedObjectContext:moc];
[theParent setValue:nil forKey:#"parent"];
// This is where you add the title from the string array
[theParent setValue:title forKey:#"name"];
[theParent setValue:[NSNumber numberWithInt:0] forKey:#"position"];
}
for(CalTask* task in allTasks)
if([oldTasks containsObject:task.title]) {
[store removeTask:task error:nil];
}
// Create a predicate for an array of names.
NSPredicate *mocPredicate = [NSPredicate predicateWithFormat:#"name IN %#", oldTasks];
[request setPredicate:mocPredicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
// Execute the fetch request put the results into array
NSArray *resultArray = [moc executeFetchRequest:request error:&error];
if (resultArray == nil)
{
// Diagnostic error handling
NSAlert *anAlert = [NSAlert alertWithError:error];
[anAlert runModal];
}
// Enumerate through the array deleting each object.
// WARNING, this will delete everything in the array, so you may want to put more checks in before doing this.
for (JGManagedObject *objectToDelete in resultArray ) {
// Delete the object.
[moc deleteObject:objectToDelete];
}
//Save the array
[namesArray writeToFile:fileName atomically:YES];
[syncButton setTitle:#"Sync Now"];
NSLog(#"Sync Completed");
}
What I've tried …
Filtering the Keypaths that call the KVO Declaration with
if ([keyPath isEqualToString:#"name"]) {
…
}
Detaching and reattaching observers with
[JGManagedObject removeObserver];
//and
[JGManagedObject addObserver];
but with that it works the first time but stops the method the second time saying that it cannot remove the observer because it is not observing, which doesn't make sense because I added the observer again the first time. That is why I left this code out of the actual method else it would stop on the second sync.
I'm not sure whats going on with this, I think I've tried everything. Whats gone wrong?
Any help would be greatly appreciated.
The problem might be here:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:#"name"]) {
[self performSelector:#selector(syncKVO:)];
}
}
You call syncKVO: everytime something happens to 'name' regardless of what it is that has actually happened to 'name'. I suggest you start using the object, change and context parameters to determine what has just happened and what action, if any, should be undertaken.
BTW, it's not considered good practice to add a lot of stuff to the app delegate. You might want to put all this syncing stuff into a proper controller class and call [NSApp delegate] when you need it.