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;
开发者_如何学C}
}
- (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];
}
精彩评论