Parsing XML with iPhone SDK 3.0, I ran into a few things worth noting. I tried building an XMLParser class written for SDK 2.x, and the build failed with an error.
- (void)parseContents:(NSURL *)url {
NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:url options:1 << 10 error:nil];
[self parseXMLData:[xmlDoc XMLData]];
[xmlDoc release];
}
Trying to parse with this simple code gives an “NSXMLDocument undeclared” error.
I looked through the SDK API and couldn’t find an NSXMLDocument class anywhere.
Setting the SDK version to 2.0 builds fine with no errors. But since I need features only available in SDK 3.0, I had to find another way around the error.
-(void)parseXMLUrl:(NSURL *)url {
NSXMLParser*p =[NSXMLParser alloc];
[p initWithContentsOfURL:url];
[p setShouldProcessNamespaces:YES];
[p setDelegate:self];
[p parse];
[p release];
}
This still failed. The reason: the XML data doesn’t fully follow spec. The XML being fetched has issues, so it needs to be cleaned up first.
There were special characters inside an XML tag that needed to be wrapped in CDATA.
NSString *xmlString = [NSString stringWithContentsOfURL:url];
NSData *xmlData = [[self sanitizeXmlString:xmlString] dataUsingEncoding:NSUTF8StringEncoding];
The goal: make the code above work.
- Write a sanitizing function that fixes the broken tag strings before parsing, as a preprocessing step.
- Use NSData’s
dataUsingEncodingto convert NSString to NSData (matching the charset). - Use NSString’s
stringWithContentsOfURLto read the file contents.
In the end, a function that wraps the contents of <link></link> in CDATA.
stringWithContentsOfURL is synchronous. It blocks on a thread until it finishes, and doesn’t let you set an async delegate. If the file is large, storage I/O is slow, or you’re loading an external file, you’d want to run that block on a separate async thread. For now, let’s keep it simple.
The sanitizing function:
-(NSString*) sanitizeLinkTag:(NSString*)string {
NSString* sanitizeLinkTag =[string stringByReplacingOccurrencesOfString:(NSString*)@"</link>" withString:(NSString*)@"]]></link>"];
sanitizeLinkTag = [sanitizeLinkTag stringByReplacingOccurrencesOfString:(NSString*)@"<link>" withString:(NSString*)@"<link><![CDATA["];
return sanitizedString;
}
For NSString’s stringByReplacingOccurrencesOfString, the first parameter is the target and the second, withString, is the replacement.
It’s not a mutating method — it returns a new String. I haven’t tried instantiating an NSString myself; the API docs just say it returns a new one. Anyway, outside of this special case — i.e. when you’re fetching properly formed XML — you need to know the XML’s structure “beforehand” in order to parse it. For a URL, check it in a browser first; for a file, read and inspect it.
NSXMLParser has four key delegate methods that handle the parsing work, and they don’t build a tree structure for you. Unlike AS3 or other libraries, they don’t give you a tree or hash-object structure automatically. More on that below.
The four most important delegate methods on NSXMLParser:
-(void)parser:(NSXMLParser*)parser parseErrorOccurred:(NSError*)parseError
-(void)parser:(NSXMLParser*)parser didStartElement:(NSString*)elementName namespaceURI:(NSString*)namespaceURI qualifiedName:(NSString*)qualifiedName attributes:(NSDictionary*)attributeDict
-(void)parser:(NSXMLParser*)parser didEndElement:(NSString*)elementName namespaceURI:(NSString*)namespaceURI qualifiedName:(NSString*)qName
-(void)parser:(NSXMLParser*)parser foundCharacters:(NSString*)string
The first, parseErrorOccurred, fires when a parsing error happens.
The second, didStartElement, fires when it hits a tag wrapped in <>.
The third, didEndElement, fires when it hits a tag wrapped in </>.
The fourth, foundCharacters, returns a String after both didStartElement and didEndElement.
The important one is the fourth, foundCharacters —
it gets delegated both after didStartElement and after didEndElement.
What that means: when it’s delegated right after didStartElement, the parameter is the data inside the element. But when it’s delegated after didEndElement, the parameter is something else (not nil, not @"")
— basically a value you can’t use (maybe a \n?).
That’s why, in a lot of sample code, the delegate methods are written using a global variable called xmlValue:
reset xmlValue to empty inside didStartElement
(xmlValue = @"";, something like that),
then in foundCharacters,
assign xmlValue = string,
and in didEndElement, use xmlValue wherever it’s needed.
That’s the pattern you need to follow. If you use the string from foundCharacters directly wherever it’s needed, you’ll end up using the garbage string value that comes through on the foundCharacters call fired after didEndParsing.
Apparently this doesn’t happen with the SDK 2.x structure that uses NSXMLDocument.
In 2.x, with NSXMLDocument, foundCharacters only fires once.
Since the XML data can be malformed, this “only fires once” behavior can also happen in SDK 3.0 by accident, so it seems safer to manage the XML data by continuously updating a global xmlValue.
And since it’s not a tree structure,
you’d use the tag name you get from elementName in didStartElement to decide
which NSMutableDictionary, array, or class acting as a struct to migrate the data into.
If an NSMutableDictionary structure looks like
car
- brand - model - color - grade
then regardless of whether the source XML was a tree or something else,
when the tag name is model, you need to directly set the model key on the dictionary inside didEndParsing.
Whether it’s color or grade, you need to convert the flat incoming data into a tree structure yourself.