Help! How to share NSXMLParser class between controllers? - iphone

I've read posts about this, and it seems pretty straight-forward. I'm pretty new to Obj-C and iPhone dev in general, so I could easily be overlooking something. I can't seem to return the NSMutableArray with the Article objects. I don't get any errors, but when I try to NSLog() some stuff I'm getting EXEC_BAD_ACCESS errors (I'm assuming a memory access issue?). I have an ArticlesParser class that does the parsing... Here's what it looks like:
// ArticlesParser.h
#import <Foundation/Foundation.h>
#import "Article.h"
#class Article;
#interface ArticlesParser : NSObject <NSXMLParserDelegate> {
NSMutableString *currentCharaters;
Article *currentArticle;
NSMutableArray *articlesCollection;
NSMutableData *xmlData;
NSURLConnection *connectionInProgress;
BOOL connectionHasCompleted;
}
#property (nonatomic, assign) BOOL connectionHasCompleted;
- (void)parseUrl:(NSString *)url;
- (void)beginParsing:(NSURL *)xmlUrl;
- (NSMutableArray *)arrayOfArticles;
#end
Here's the implementation...
// ArticlesParser.m
#import "ArticlesParser.h"
#implementation ArticlesParser
#synthesize connectionHasCompleted;
#pragma mark -
#pragma mark Parsing methods
- (void)parseUrl:(NSString *)url
{
[self setConnectionHasCompleted:NO];
NSURL *xmlUrl = [NSURL URLWithString:url];
[self beginParsing:xmlUrl];
}
- (void)beginParsing:(NSURL *)xmlUrl
{
[articlesCollection removeAllObjects];
articlesCollection = [[NSMutableArray alloc] init];
NSURLRequest *request = [NSURLRequest requestWithURL:xmlUrl cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30];
// clear existing connection if there is one
if (connectionInProgress) {
[connectionInProgress cancel];
[connectionInProgress release];
}
[xmlData release];
xmlData = [[NSMutableData alloc] init];
// asynchronous connection
connectionInProgress = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
}
- (NSMutableArray *)arrayOfArticles
{
// NOT RETURNING ANYTHING
return articlesCollection;
}
#pragma mark -
#pragma mark NSXMLParserDelegate methods
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[xmlData appendData:data];
}
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
if ([elementName isEqual:#"article"]) {
currentArticle = [[Article alloc] init];
return;
}
if ([elementName isEqual:#"title"]) {
currentCharaters = [[NSMutableString alloc] init];
return;
}
if ([elementName isEqual:#"last_updated"]) {
currentCharaters = [[NSMutableString alloc] init];
return;
}
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
[currentCharaters appendString:string];
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
if ([elementName isEqual:#"article"]) {
[articlesCollection addObject:currentArticle];
[currentArticle release], currentArticle = nil;
return;
}
if ([elementName isEqual:#"title"]) {
[currentArticle setTitle:currentCharaters];
[currentCharaters release], currentCharaters = nil;
return;
}
if ([elementName isEqual:#"last_updated"]) {
[currentArticle setLastModified:currentCharaters];
[currentCharaters release], currentCharaters = nil;
return;
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:xmlData];
[parser setDelegate:self];
[parser parse];
[parser release];
[self setConnectionHasCompleted:YES];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[currentArticle release];
currentArticle = nil;
[currentCharaters release];
currentCharaters = nil;
[articlesCollection release];
articlesCollection = nil;
[connectionInProgress release];
connectionInProgress = nil;
[xmlData release];
xmlData = nil;
NSLog(#"connection failed: %#", [error localizedDescription]);
}
#end
I know that the actual parsing works because I did have this directly in my view controller and everything worked fine. But now I want to access basically the same thing from another controller, only the URL is different (returns the same formatted XML though).
Here's how I'm trying to make use of this class in my controller:
// instance method called within an articles controller
// that is to load the results in a table view
- (void)loadArticles
{
// (leaving off the URL because it's not important)
NSString *urlToRequest = [NSString stringWithFormat:#"...", [self letterToList]];
ArticlesParser *aParser = [[ArticlesParser alloc] init];
// initiate the parsing
[aParser parseUrl:urlToRequest];
// load up the articles ivar so the tableview can
// make use of it to load its cells
articles = [aParser arrayOfArticles];
}
Is there something obvious that I'm missing? Is this even a good way to share the NSXMLParser code?
I'm pulling my hair out over this one... thanks in advance!

What is it you're trying to NSLog that generates the EXEC_BAD_ACCESS error? Looking at your code your call to arrayOfArticles should return an NSMutableArray with no elements, so e.g. something like this would understandably give an EXEC_BAD_ACCESS:
NSLog(#"%#", [[articles objectAtIndex:0] description]); // index out of bounds
By having your XML parser class also responsible for fetching the data it's going to parse (using NSURLConnection) you've made it asynchronous, which means it's no longer suitable to be used like this:
ArticlesParser *ap = [[[ArticlesParser alloc] init] autorelease];
[ap parseURL:#"http://example.com/foo"];
NSArray *anArray = [ap arrayOfArticles];
anArray is now an empty array, and will only be populated at some indeterminate point in the future, if at all - and you can't detect when that time comes without polling the array. Urgh! :)
There are a couple of ways you might get around this. One approach is to have your XML Parser class declare delegate methods, offering callbacks for when the XML data has been fully fetched and parsed and when error conditions occur (in much the same way that the delegate methods in NSURLConnection work). Another approach is to have your XML Parser class be a simple (synchronous) XML parser, and move the asynchronous data-fetching code to outside your class.

There are a few things I see a problem with off the top of my head.
First, you need to either copy or retain the return from arrayOfArticles if you're going to hang on to it and use it later.
articles = [[aParser arrayOfArticles] copy];
You then of course need to make sure you release it later whenever it's appropriate.
Second, as it's written loadArticles actually leaks the ArticleParser it creates so you need to call [aParser release] at the end of the method.
The fact that you have to release the parser, which created the array, is what makes it necessary to retain/copy the return value. As soon as the ArticlesParser is deallocated it'll release it's internal articlesCollection and deallocate it if that was the last reference. Since your arrayOfArticles method hands out that reference to others, they need to copy the array or retain the reference to keep it alive after the ArticlesParser that created it dies.
Finally, you're downloading the data asynchronously, but you're calling arrayOfArticles immediately after calling parseUrl:. This is never going to result in you getting anything useful because no data has been downloaded or parsed yet. You need your ArticlesParser to provide some way to notify interested parties when it is done parsing the downloaded data and THEN they can call arrayOfArticles to get the data.
EDIT:
One way to deal with the notification would be to create a delegate protocol, add a delegate property to ArticlesParser, have the controller set itself as the value of that property, and have the parser call the delegate's method when it's done.
For example:
// ArticlesParser.h
#import <Foundation/Foundation.h>
#import "Article.h"
#class Article;
#class ArticlesParser;
#protocol ArticlesParserDelegate <NSObject> {
- (void)parserDidFinish:(ArticlesParser*)parser;
- (void)parser:(ArticlesParser*)parser didFailWithError:(NSError*)error;
#end
#interface ArticlesParser : NSObject <NSXMLParserDelegate> {
id<ArticlesParserDelegate> delegate;
// ... the rest the same ...
}
#property (nonatomic, assign) id<ArticlesParserDelegate> delegate;
// ... the rest the same ...
#end
// ArticlesParser.m
// ... the same as you have, but with this stuff added ...
#synthesize delegate;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// ... same as you have, add this at end...
[delegate parserDidFinish:self];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
// ... same as you have, add this at end...
[delegate parser:self didFailWithError:error];
}

Related

How to get the XML parsed images into array and use the array in all view controllers using iphone sdk

I have an XML file in URL where i can parse the data and get the images from the XML file. But i don't know how to use the images in other view controllers. I got an idea to store the images in array, but how can i use the array of images in other view controllers. Is it possible to store the XML parsed images in array.Am i parsing the XML correctly? Kindly suggest me an idea.
My XML Parser
//egsBase.h
#import <Foundation/Foundation.h>
#interface egsBase : NSObject{
IBOutlet UIImage *img;
}
#property(nonatomic,retain) IBOutlet UIImage *img;
#end
//egsBase.m
#import "egsBase.h"
#implementation egsBase
#synthesize img;
#end
//egsXMLParser.h
#import <Foundation/Foundation.h>
#import "egsBase.h"
#interface egsXMLParser : NSObject <NSXMLParserDelegate>{
NSMutableString *currentNodeContent;
NSMutableArray *tweets;
NSXMLParser *parser;
egsBase *currentTweet;
}
#property(nonatomic,readonly) NSMutableArray *tweets;
-(id) loadXMLByURL:(NSString *) urlString;
#end
//egsXMLParser.m
#import "egsXMLParser.h"
#implementation egsXMLParser
#synthesize tweets;
-(id) loadXMLByURL:(NSString *)urlString{
tweets = [[NSMutableArray alloc] init];
NSURL *url = [NSURL URLWithString:urlString];
NSData *data = [[NSData alloc] initWithContentsOfURL:url];
parser = [[NSXMLParser alloc] initWithData:data];
parser.delegate = self;
[parser parse];
return self;
}
-(void) parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
if([elementName isEqualToString:#"Products"]){
currentTweet = [egsBase alloc];
}
}
-(void) parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
if([elementName isEqualToString:#"img"])
{
currentTweet.img=[UIImage imageWithData: [NSData dataWithContentsOfURL: [NSURL URLWithString:currentNodeContent]]]; [tweets addObject:currentTweet.img];
}
}
- (void) parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
currentNodeContent = (NSMutableString *) [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
#end
Are you parsing this correctly? There are a lot of issues, some substantive, some more stylistic. First, a couple of substantive observations:
Your foundCharacters assumes that it will be called only once for each element. For something like a URL that will generally be true, but you're not guaranteed of this fact. It should just be appending characters to a string, and the subsequent storage of the final string and the clean up should happen in didEndElement.
Your foundCharacter is also storing results whether you're retrieving data for an element you care about or not (and worse, characters between elements). Only store results for foundCharacter if you're between a didStartElement and didEndElement for an element name you care about.
Your didStartElement is performing the alloc, but it is not doing the init, too. You should always do the init at the same time. You can do additional setting of properties, later, too, but never neglect to call the appropriate init method.
Your didEndElement is retrieving an image from the server. You really should restrict the retrieval of data to just the XML, itself (e.g. in the case of an image, the URL for an image), and not go retrieving additional items from the server. This is especially important as you consider more sophisticated designs, where to minimize memory hits and improve user response time, you may want to employ lazy loading of images, meaning that you want to only retrieve the URL from the XML, and let the UI handle if and when images are retrieved. You'll regret doing the image retrieval here as you start doing more sophisticated development in the future.
You have an imbalance between the elementName checking you're doing in didStartElement and didEndElement. For all of the elements for which you're retrieving data, you should balance the calls. I know you gave us an example where you were only retrieving a single element from the XML for reasons of simplicity, but in my example below, I'll assume we're retrieving two elements, and the importance of balancing these will become apparent.
For more stylistic observations:
By convention, variable names generally start with lowercase letters. Class names generally start with upper case letter. I'm assuming that "EGS" is an established class prefix (e.g. your initials or your firm's acronym), in which case the class names would probably be EGSBase and EGSParser. By the way, I don't really like the name EGSBase because it's a class designed to capture a particular type of XML data, but has a generic name. Either use a generic data structure like a NSMutableDictionary or use a custom class and give it a meaningful name (e.g. based upon your references to "products", I'm assuming is is a product, and thus I've renamed it EGSProduct in my example below.) Call it whatever you want, but give it a meaningful name if you're using meaningful property names.
I notice that you're defining instance variables to back your properties. While that was common convention a year or two ago, latest versions of compilers make this unnecessary, and it's even inadvisable. What you've got works, but it's no longer considered best practice, and is inadvisable (since you can, with a single typo, end up with duplicative instance variables and get very strange behavior). Bottom line, do not define instance variables for your properties. Let the compiler do that for you.
I notice you've defined img in egsBase to be an IBOutlet, whereas I find it extremely unlikely that you really have that linked to anything in Interface Builder. First, it's not even a UIView descendent (it's a UIImage, not a UIImageView). Second, egsBase is not, itself, a class that you'd be using for an Interface Builder control. Bottom line, this isn't an Interface Builder outlet, so it's just confusing to use IBOutlet for img.
I notice that you are using retain. If you're writing ARC code, that should probably be strong (even if you're targeting iOS 4.3). If you're not writing ARC code, you have leaks elsewhere in your code.
You are manually synthesizing your properties. You can do that, but it's not necessary. Also, best practice nowadays is to have your property's instance variables have a leading underscore. Note, if you omit the #synthesize statement, it will synthesize the instance variable with the leading underscore for you. There is some debate in the field as to whether you should use properties exclusively (which I've done below) or use instance variables. I don't care too much, though emerging conventions might lean towards a more extensive use of properties. If you use properties, though (a) don't use the accessor methods in the initializer and dealloc methods; and (b) always use the accessor methods when setting properties.
I notice that you're defining properties and instance variables (for the parser, in particular) which are private implementation details. The emerging standard here is to limit the properties defined in the .h as those that other classes will need access to. Any other properties which are part of the private implementation of a class are generally defined in private class extension in the .m file itself. It's a minor thing, but if you observe this practice, you'll find it easier to use your classes in the future, where you (or other developers) won't get confused as to what's part of the public interface, and what's part of the private implementation.
I notice that your loadXMLByURL returns a pointer to the class itself. Generally only classes that init objects or create new objects would return a pointer to themselves.
Your loadXMLByURL should probably should return either (a) a pointer to the NSMutableArray that you constructed (returning nil on an error); or (b) a BOOL success/failure value.
For your custom classes, like EGSBase, it's useful to write a description method, so you can easily NSLog them.
So, let me give you an example. Let's assume you have an XML that looks like:
<Products>
<products id="0">
<name>name1</name>
<img id="1">http://opentestdrive.com/images/first.png</img>
</products>
<products id="1">
<name>name2</name>
<img id="2">http://opentestdrive.com/images/second.png</img>
</products>
<products id="2">
<name>name3</name>
<img id="3">http://opentestdrive.com/images/img1.png</img>
</products>
<products id="3">
<name>name4</name>
<img id="4">http://opentestdrive.com/images/img2.png</img>
<img-subproduct id="0">http://opentestdrive.com/images/img5.png</img-subproduct>
<img-subproduct id="1">http://opentestdrive.com/images/img4.png</img-subproduct>
</products>
<products id="4">
<name>name5</name>
<img id="5">http://opentestdrive.com/images/img3.png</img>
<img-subproduct id="2">http://opentestdrive.com/images/img3.png</img-subproduct>
<img-subproduct id="3">http://opentestdrive.com/images/img2.png</img-subproduct>
</products>
<products id="5">
<name>name6</name>
<img id="6">http://opentestdrive.com/images/img4.png</img>
</products>
<products id="6">
<name>name7</name>
<img id="7">http://opentestdrive.com/images/img5.png</img>
</products>
</Products>
Then you EGSProduct would be defined as follows:
// EGSProduct.h
#import <Foundation/Foundation.h>
#interface EGSProduct : NSObject
#property (nonatomic, strong) NSString *name;
#property (nonatomic, strong) NSString *imageUrlString;
#property (nonatomic, strong) NSString *imageIdentifier;
#property (nonatomic, strong) NSMutableArray *subProducts;
#end
and
// EGSProduct.m
#import "EGSProduct.h"
#implementation EGSProduct
- (NSString *)description
{
NSMutableString *result = [NSMutableString stringWithFormat:#"<EGSProduct %p; name='%#'; imageIdentifier='%#'; imageUrlString='%#'; subProducts=", self, self.name, self.imageIdentifier, self.imageUrlString];
NSMutableArray *subProductDescriptions = [NSMutableArray array];
for (EGSProduct *subProduct in self.subProducts)
{
[subProductDescriptions addObject:[subProduct description]];
}
[result appendFormat:#"%#>", [subProductDescriptions componentsJoinedByString:#"; "]];
return result;
}
#end
And your parser might look like:
// EGSParser.h
#import <Foundation/Foundation.h>
#interface EGSParser : NSObject
#property (nonatomic, strong) NSMutableArray *products;
- (BOOL)loadXMLByURL:(NSURL *)url;
#end
And
// EGSParser.m
#import "EGSParser.h"
#import "EGSProduct.h"
#interface EGSParser () <NSXMLParserDelegate>
#property (nonatomic, strong) NSXMLParser *parser;
#property (nonatomic, strong) NSMutableString *currentElementContent;
#property (nonatomic, strong) EGSProduct *currentProduct;
#property (nonatomic, strong) EGSProduct *currentSubProduct;
#end
#implementation EGSParser
- (BOOL)loadXMLByURL:(NSURL *)url
{
self.parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
self.parser.delegate = self;
return [self.parser parse];
}
#pragma mark - NSXMLParser
- (void)parserDidStartDocument:(NSXMLParser *)parser
{
self.products = [[NSMutableArray alloc] init];
}
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
if ([elementName isEqualToString:#"products"])
{
self.currentProduct = [[EGSProduct alloc] init];
self.currentProduct.imageIdentifier = attributeDict[#"id"];
}
else if ([elementName isEqualToString:#"img-subproduct"])
{
self.currentSubProduct = [[EGSProduct alloc] init];
self.currentSubProduct.imageIdentifier = attributeDict[#"id"];
if (self.currentProduct.subProducts == nil)
{
self.currentProduct.subProducts = [NSMutableArray array];
}
self.currentSubProduct.imageIdentifier = attributeDict[#"id"];
self.currentElementContent = [[NSMutableString alloc] init];
}
else if ([elementName isEqualToString:#"name"] || [elementName isEqualToString:#"img"])
{
self.currentElementContent = [[NSMutableString alloc] init];
}
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
// note, this construct of checking to see if it's nil is actually not needed
// (since sending a message to a nil object, by definition, does nothing)
// but I wanted to draw your attention to the fact that we set `currentElementContent`
// in `didStartElement` and, after saving it in our class, we set it to nil in
// `didEndElement`
if (self.currentElementContent != nil)
[self.currentElementContent appendString:string];
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
if ([elementName isEqualToString:#"name"])
{
self.currentProduct.name = self.currentElementContent; // save the product name
self.currentElementContent = nil; // reset our `currentElementContent`
}
else if ([elementName isEqualToString:#"img"])
{
self.currentProduct.imageUrlString = [self.currentElementContent stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
self.currentElementContent = nil;
}
else if ([elementName isEqualToString:#"img-subproduct"])
{
self.currentSubProduct.imageUrlString = self.currentElementContent;
self.currentElementContent = nil;
[self.currentProduct.subProducts addObject:self.currentSubProduct];
self.currentSubProduct = nil;
}
else if ([elementName isEqualToString:#"products"])
{
[self.products addObject:self.currentProduct];
self.currentProduct = nil;
}
}
#end
To use this class you might do something like the following. If you want to hang on to the XML results after the parser is done (and goes out of scope), you'd just have a class property (in the example below, xmlProductsResults. If you want this to be accessible to a variety of different view controllers, you'd save this model data in some shared class (e.g. a model singleton), a property of your app delegate, or pass it as a parameter from view controller to view controller.
NSURL *url = [NSURL URLWithString:#"http://opentestdrive.com/Products.xml"];
EGSParser *parser = [[EGSParser alloc] init];
if ([parser loadXMLByURL:url])
{
NSLog(#"success; products=%#", parser.products);
self.xmlProductsResults = parser.products;
}
else
{
NSLog(#"fail");
}
You could do a lot more with your parser (more robust validation of poorly formed XML files, reporting the errors, if any, back, etc.). Personally, I've done enough XML parsing now that I now employ a very generic parser that can parse most of the XML files I'm dealing with in a few lines, enjoying maximum reuse of the parser class, but that's a bridge too far for this conversation. I've already gone too far in this discussion.

How can I cancel an asynchronous call through NSURLConnection sendAsynchronousRequest?

I've got a web service call performing some validation on user input in real time. I'd like to use [NSURLConnection sendAsynchronousRequest] on the validation (which was introduced in iOS 5), but cancel it if the user changes the input field content in the mean time. What is the best way to cancel a current request?
It doesn't appear that there is a good way to do this. The solution seems to be to not use the new [NSURLConnection sendAsynchronousRequest] in situations in which you need to cancel the request.
I've managed to do this by placing the sendAsynchronousRequest method in a separate DownloadWrapper class, as follows:
//
// DownloadWrapper.h
//
// Created by Ahmed Khalaf on 16/12/11.
// Copyright (c) 2011 arkuana. All rights reserved.
//
#import <Foundation/Foundation.h>
#protocol DownloadWrapperDelegate
- (void)receivedData:(NSData *)data;
- (void)emptyReply;
- (void)timedOut;
- (void)downloadError:(NSError *)error;
#end
#interface DownloadWrapper : NSObject {
id<DownloadWrapperDelegate> delegate;
}
#property(nonatomic, retain) id<DownloadWrapperDelegate> delegate;
- (void)downloadContentsOfURL:(NSString *)urlString;
#end
#implementation DownloadWrapper
#synthesize delegate;
- (void)downloadContentsOfURL:(NSString *)urlString
{
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:TIMEOUT_INTERVAL];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] > 0 && error == nil)
[delegate receivedData:data];
else if ([data length] == 0 && error == nil)
[delegate emptyReply];
else if (error != nil && error.code == ERROR_CODE_TIMEOUT)
[delegate timedOut];
else if (error != nil)
[delegate downloadError:error];
}];
}
#end
To utilise this class, I do the following, in addition to declaring the DownloadWrapper *downloadWrapper variable (in the interface declaration) and implementing the protocol methods which handles the response or a lack of one:
NSString *urlString = #"http://yoursite.com/page/to/download.html";
downloadWrapper = [DownloadWrapper alloc];
downloadWrapper.delegate = self;
[downloadWrapper downloadContentsOfURL:urlString];
Then I simply do the following to 'cancel' the connection when the view is about to disappear:
- (void)viewDidUnload
{
[super viewDidUnload];
downloadWrapper = nil;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[downloadWrapper setDelegate:nil];
}
It's as simple as that. This would hopefully mimic the documented cancel method, which states that it does the following:
Once this method is called, the receiver’s delegate will no longer
receive any messages for this NSURLConnection.
I was concerned that this (somewhat naive) method means that the packets of data would still come through in response to our URL request - only that we're no longer 'listening in' as the delegate. But then I realised that once the URL request was sent through, there's really no way of stopping the response from coming back to us - we can only disregard it (if not at this level, then still at some lower level in the network hierarchy). Please correct me if I'm wrong.
Either way, hope this helps.

NSArray gives NSZombie error

I am trying to pass a single array object (that is a nsdictionary of several values) back to my main view.
basicly when I set the view up I parse some xml into an array of dictionaries. I then set up my tableview with one of the values inside the NSdictionary, this value is also used to set up the alphabetical scroller and section titles. (this is done in a method I created)
At the end of that method I call [self.tableView reloadData]; every thing loads up perfectly and everything displays fine.
Now what I am trying to do is set it up so that when a cell is selected, I check the value inside the cell.textlabel and use that as a predicate to check against my array of dictionaries once It finds the corresponding entry I want to pass that dictionary up to the main view with a delegate I have made.
however I am getting a error, that I think might be happening due to my reloadData.. but am not sure.
This is what my predicate looks like.
NSPredicate *pred = [NSPredicate predicateWithFormat:#"%K like %#",#"MANUFACTURER",cell.textLabel.text];
NSArray *filter = [myDataArray filteredArrayUsingPredicate:pred]; //error happens here
//check to see if the value is the correct one
NSLog(#"My Filtered array = %#", filter);
//once problem has been found set up the delegate here.
and this is the error message I receive.
2011-10-31 10:43:57.333 code[5812:207] *** -[__NSArrayM filteredArrayUsingPredicate:]: message sent to deallocated instance 0x6874210
myDataArray is created in the NSXMLParser delegates as listed below.
//.h
NSMutableArray *myDataArray;
}
#property (nonatomic, retain) NSMutableArray *myDataArray;
//.m
#pragma mark - Parsing lifecycle
- (void)startTheParsingProcess:(NSData *)parserData
{
//myDataArray = [NSMutableArray arrayWithCapacity:8]; // not even sure if this is needed as its declared later on.
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:parserData]; //parserData passed to NSXMLParser delegate which starts the parsing process
[parser setDelegate:self];
[parser parse]; // starts the event-driven parsing operation.
[parser release];
}
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
if([elementName isEqualToString:#"Row"])
{
manufactureMutableDictionary = [[NSMutableDictionary alloc] initWithDictionary:attributeDict];
}
if([elementName isEqualToString:#"Rows"])
{
myDataArray = [NSMutableArray arrayWithCapacity:8];
}
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
if([elementName isEqualToString:#"Row"])
{
[myDataArray addObject:manufactureMutableDictionary];
}
[manufactureMutableDictionary release];
manufactureMutableDictionary = nil;
}
Any help would be greatly appreciated, also do you think I am going about passing all the values of the dictionary the right way?
You are using an autoreleased array
myDataArray = [NSMutableArray arrayWithCapacity:8];
You have properties set up so use them e.g.
self.myDataArray = [NSMutableArray arrayWithCapacity:8];
or even better
NSMutableArray *tmpMyDataArray = [[NSMutableArray alloc] initWithCapacity:8];
self.myDataArray = tmpMyDataArray;
[tmpMyDataArray release]; tmpMyDataArray = nil;

Objective-C: Asynchronously populate UITableView - how to do this?

I can't seem to find any info on this question, so I thought I'd ask the community.
Basically, I have a UITableView and I want to show an activity indicator while its data is loading from my server.
Here is some example code of what I'm trying to do (I'm using ASIHttpRequest).
//self.listData = [[NSArray alloc] initWithObjects:#"Red", #"Green", #"Blue", #"Indigo", #"Violet", nil]; //this works
NSString *urlStr=[[NSString alloc] initWithFormat:#"http://www.google.com"]; //some slow request
NSURL *url=[NSURL URLWithString:urlStr];
__block ASIHTTPRequest *request=[ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request setCompletionBlock:^{
self.listData = [[NSArray alloc] initWithObjects:#"Red", #"Green", #"Blue", #"Indigo", #"Violet", nil]; //this doesn't work...
[table reloadData];
}];
[request setFailedBlock:^{
}];
[request startAsynchronous];
The dummy request to google.com does nothing - it just creates a delay and in the response I hope to repopulate the table with some JSON response from my own website.
But when I try to populate the table with the colours, nothing happens! I just get a blank table... If I uncomment the line above, it works fine, it's just on http responses things don't work for me.
Any suggestions greatly appreciated.
Edit:
I did a [self.tableView reloadData]; and now it works...
Stop using ASIHTTPRequest. NSURLConnection is not hard to use and will result in better, more performant code.
Your JSON response should be fed into a data structure not the UI. I recommend Core Data.
The data structure should feed your UITableView. Again, I recommend Core Data.
I would suggest reviewing how MVC works, you are short circuiting the design and that is the core problem.
SPOILER
Here is a more detailed how to. First you want the data retrieval to be async. Easiest and most reusable way to do that is build a simple NSOperation subclass.
#class CIMGFSimpleDownloadOperation;
#protocol CIMGFSimpleDownloadDelegate <NSObject>
- (void)operation:(CIMGFSimpleDownloadOperation*)operation didCompleteWithData:(NSData*)data;
- (void)operation:(CIMGFSimpleDownloadOperation*)operation didFailWithError:(NSError*)error;
#end
#interface CIMGFSimpleDownloadOperation : NSOperation
#property (nonatomic, assign) NSInteger statusCode;
- (id)initWithURLRequest:(NSURLRequest*)request andDelegate:(id<CIMGFSimpleDownloadDelegate>)delegate;
#end
This subclass is the most basic way to download something from a URL. Construct it with a NSURLRequest and a delegate. It will call back on a success or failure. The implementation is only slightly longer.
#import "CIMGFSimpleDownloadOperation.h"
#interface CIMGFSimpleDownloadOperation()
#property (nonatomic, retain) NSURLRequest *request;
#property (nonatomic, retain) NSMutableData *data;
#property (nonatomic, assign) id<CIMGFSimpleDownloadDelegate> delegate;
#end
#implementation CIMGFSimpleDownloadOperation
- (id)initWithURLRequest:(NSURLRequest*)request andDelegate:(id<CIMGFSimpleDownloadDelegate>)delegate
{
if (!(self = [super init])) return nil;
[self setDelegate:delegate];
[self setRequest:request];
return self;
}
- (void)dealloc
{
[self setDelegate:nil];
[self setRequest:nil];
[self setData:nil];
[super dealloc];
}
- (void)main
{
[NSURLConnection connectionWithRequest:[self request] delegate:self];
CFRunLoopRun();
}
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSHTTPURLResponse*)resp
{
[self setStatusCode:[resp statusCode]];
[self setData:[NSMutableData data]];
}
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)newData
{
[[self data] appendData:newData];
}
- (void)connectionDidFinishLoading:(NSURLConnection*)connection
{
[[self delegate] operation:self didCompleteWithData:[self data]];
CFRunLoopStop(CFRunLoopGetCurrent());
}
- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
{
[[self delegate] operation:self didFailWithError:error];
CFRunLoopStop(CFRunLoopGetCurrent());
}
#synthesize delegate;
#synthesize request;
#synthesize data;
#synthesize statusCode;
#end
Now this class is VERY reusable. There are other delegate methods for NSURLConnection that you can add depending on your needs. NSURLConnection can handle redirects, authentication, etc. I strongly suggest you look into its documentation.
From here you can either spin off the CIMGFSimpleDownloadOperation from your UITableViewController or from another part of your application. For this demonstration we will do it in the UITableViewController. Depending on your application needs you can kick off the data download wherever makes sense. For this example we will kick it off when the view appears.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSURLRequest *request = ...;
CIMGFSimpleDownloadOperation *op = [[CIMGFSimpleDownloadOperation alloc] initWithURLRequest:request andDelegate:self];
[[NSOperationQueue mainQueue] addOperation:op];
[self setDownloadOperation:op]; //Hold onto a reference in case we want to cancel it
[op release], op = nil;
}
Now when the view appears an async call will go and download the content of the URL. In this code that will either pass or fail. The failure first:
- (void)operation:(CIMGFSimpleDownloadOperation*)operation didFailWithError:(NSError*)error;
{
[self setDownloadOperation:nil];
NSLog(#"Failure to download: %#\n%#", [error localizedDescription], [error userInfo]);
}
On success we need to parse the data that came back.
- (void)operation:(CIMGFSimpleDownloadOperation*)operation didCompleteWithData:(NSData*)data;
{
[self setDownloadOperation:nil];
NSLog(#"Download complete");
//1. Massage the data into whatever we want, Core Data, an array, whatever
//2. Update the UITableViewDataSource with the new data
//Note: We MIGHT be on a background thread here.
if ([NSThread isMainThread]) {
[[self tableView] reloadData];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[[self tableView] reloadData];
});
}
}
And done. A few more lines of code for you to write but it replaces 13K+ lines of code that gets imported with ASI resulting in a smaller, leaner, faster application. And more importantly it is an app that you understand every single line of code.
This is the problem
request setCompletionBlock:^{
self.listData = [[NSArray alloc] initWithObjects:#"Red", #"Green", #"Blue", #"Indigo", #"Violet", nil]; //this doesn't work...
[table performSelectorOnMainThread:#selector(reloadTable) withObject:nil waitUntilDone:NO];
}];
The reload table needs to be done on the main thread.
I tried NWCoder's solution, and it didn't work because its calling the wrong method. This is what i used.
[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];

How to return an object from a class that uses NSURLConnection and it's delegate classes?

I'm in the process of trying to move code from a UITableViewController class to a "helper" class.
The code utilizes NSURLConnection to grab and parse JSON and then populate an NSMutableArray.
What I'd like to do is call a method in my helper class that returns a NSMutableArray. What I don't understand is how to return the array from the connectionDidFinishLoading delegate class of NSURLConnection (where the array is actually built) as though it was from the originally called method that started the connection. In other words, how does the method that calls NSURLConnection get control back so it can return a value from the whole operation?
Here are the relevant methods from the helper class. How do I get the getMovies method to return the listOfMovies that is built in the connectionDidFinishLoading delegate class?
-(NSMutableArray)getMovies:(NSURL*)url {
responseData = [[NSMutableData data] retain];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//NSURLRequest* request = [NSURLRequest requestWithURL: url cachePolicy: NSURLRequestUseProtocolCachePolicy timeoutInterval: 30.0];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[responseData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[responseData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
//TODO error handling for connection
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//---initialize the array---
listOfMovies = [[NSMutableArray alloc] init];
tmdbMovies = [[NSArray alloc] init];
posters = [[NSArray alloc] init];
thumbs = [[NSDictionary alloc] init];
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
SBJsonParser *json = [[SBJsonParser new] autorelease];
tmdbMovies = [json objectWithString:responseString];
// loop through all the top level elements in JSON
for (id movie in tmdbMovies) {
// 0 - Name
// 1 - Meta
// 2 - Url
if ((NSNull *)[movie objectForKey:#"name"] != [NSNull null]) {
if (![[movie objectForKey:#"name"] isEqualToString:#""]) {
name = [movie objectForKey:#"name"];
}
}
if ((NSNull *)[movie objectForKey:#"info"] != [NSNull null]) {
if (![[movie objectForKey:#"info"] isEqualToString:#""]) {
meta = [movie objectForKey:#"info"];
}
}
if ((NSNull *)[movie objectForKey:#"thumb"] != [NSNull null]) {
if (![[movie objectForKey:#"thumb"] isEqualToString:#""]) {
thumbUrl = [movie objectForKey:#"thumb"];
}
}
NSLog(#"Name: %#", name);
NSLog(#"Info: %#", meta);
NSLog(#"Thumb: %#", thumbUrl);
NSMutableArray *movieData = [[NSMutableArray alloc] initWithObjects:name,meta,thumbUrl,nil];
// add movieData array to listOfJMovies array
[listOfMovies addObject:movieData];
[movieData release];
}
//FIXME: Connection warning
if (connection!=nil) {
[connection release];
}
[responseData release];
[responseString release];
}
What you really need to do here is create a #protocol that creates a delegate for your helper class. Then change -(NSMutableArray)getMovies:(NSURL*)url to -(void)getMovies:(NSURL*)url
The class that is calling your helper method needs to implement your helper method's delegate.
Then - (void)connectionDidFinishLoading:(NSURLConnection *)connection calls the delegate method(s). It's best to have a one for success and one for failure.
=Update Begin=
You will need to also define an id delegate in your helper file which the calling class sets to self after init but before calling -(void)getMovies:(NSURL*)url. That way the helper file knows where to call back to.
getMovies *movieListCall = [[getMovies alloc] init];
movieListCall.delegate = self;
[movieListCall getMovies:<your NSURL goes here>];
You will see some additional lines for the inclusion of a delegate in both the getMovies.h and getMovies.m files.
=Update End=
in your getMovies.h file add:
#protocol getMoviesDelegate
#required
- (void)getMoviesSucceeded:(NSMutableArray *)movieArray;
- (void)getMoviesFailed:(NSString *)failedMessage;
#end
#interface getMovies : NSOBject {
id delegate;
}
#property (nonatomic, assign) id delegate;
in your getMovies.m file add:
#synthesize delegate;
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
//TODO error handling for connection
if ([delegate respondsToSelector:#selector(getMoviesFailed:)]) {
[delegate getMoviesFailed:[error localizedDescription]];
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//finishes with
if ([delegate respondsToSelector:#selector(getMoviesSucceeded:)]) {
[delegate getMoviesSucceeded:listOfMovies];
}
}
update your calling class .h file to use getMoviesDelegate:
#interface MoviesView : UIViewController <getMoviesDelegate>{
.
.
.
}
add the getMoviesDelegate methods to your calling class .m file
- (void)getMoviesSucceeded:(NSMutableArray *)movieArray {
//deal with movieArray here
}
- (void)getMoviesFailed:(NSString *)failedMessage {
//deal with failure here
}
This is not tested but hopefully gives you a road map to work with.
Protocols are nice because you can make both required and optional delegate methods and it helps in refining your helper methods to become very reusable across projects. The compiler will also warn you if you have implemented a protocol but not implemented the protocol's required delegate methods. If you follow this path be sure to use conformsToProtocol: and respondsToSelector:
Fundamentally, what's happening is that you're starting an asynchronous network load (asynchronous is the right way to do this, almost assuredly), and then you need some way to resume whatever operation you were doing before the load began. You have a few options:
Create your own delegate protocol. Your UITableViewController would then set itself as the helper's delegate, and the helper would call helperDidLoad or whatever you named that method. There's more information on writing delegates in the Cocoa Programming Guide.
Use blocks and continuation passing style. This is a bit more advanced but I like it. In your UITableViewController you'd write something like this:
[helper doSomething:^ (id loaded) {
[modelObject refresh:loaded]; // or whatever you need to do
}];
And then in your helper you'd write:
- (void)doSomething:(void ^ (id))continuation {
_continuation = continuation;
//kick off network load
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
_continuation(_data);
}
Use notifications. Read the NSNotificationCenter docs.
Use KVO. The KVO programming guide has a lot of good info on Key-Value Observing.
How to i get the getMovies method to return the listOfMovies that is built in the connectionDidFinishLoading delegate class?
I'm going to argue that you should not do that.
Network requests should be made asynchronously. If your getMovies were to make a synchronous request and return only when it had data you would block that entire thread while you waiting for a network connection to finish. This is a bad idea in general and a terrible idea if your main thread is calling getMovies. Blocking the main thread will prevent you from responding to touches or updating the UI, your app will appear frozen, and the OS will terminate it if your users don't quit in frustration first.
Instead have the helper class notify the caller when data is available (or when it failed to retrieve data) through a delegate call back, notification, KVO, or whatever mechanism you prefer.
Here are the steps, pseudocode like style:
[helperInstance setDelegate:self]; // where self is your UITableViewController class
in your helper class, in the connectionDidFinishLoading do something like this:
[delegate finishedLoadingData:JSONData];
Also you can define a protocol for your delegate, and the declare the delegate like this in your helper class:
#property (nonatomic, assign) id<YourProtocol> delegate;
Hope this helps,
Moszi