Learn About the Nsxmlparser in iOS

Learn about the NSXMLParser in iOS

There were two fundamentally different components to this question, parsing and annotating maps. I'll focus on the map annotation here, as I think I covered the parsing questions here: Try to load a created Map in MKMapView. At the end of this answer, though, I include some references to Apple's parsing documentation if you're just trying to get your arms around NSXMLParser.

Annotating maps

The common model in mapping apps on the iPhone is to not show a popover with rich content on the maps view itself, but rather, due to the iPhone's limited real estate, to just show the standard callout, but set the rightCalloutAccessoryView to be a disclosure indicator, which, if you tap on it, will segue to that next view with the details. Thus, by using UIMapViewDelegate methods, you can have a mapView:viewForAnnotation: that says:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
MKAnnotationView *annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"loc"];
annotationView.canShowCallout = YES;
annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];

return annotationView;
}

This yields the following user interface:

right callout accessory

You can then have a mapView:annotationView:calloutAccessoryControlTapped: like so:

- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
[self performSegueWithIdentifier:@"DetailsIphone" sender:view];
}

You can use that to go to your details screen. (I'm just doing a modal segue to a view controller with a web view, passing the annotation in the prepareForSegue, and the viewDidLoad is grabbing the html, etc. The details here are unremarkable. I assume you can transition to your own details screen and design something prettier than this quick and dirty web view ... I'm just demonstrating that we can grab the HTML for a placemark out of the KML file):

iPhone details screen

So, while the iPhone really shouldn't be using popovers on the map itself, on the iPad, you can use them. You can create the rightCalloutAccessoryView in a similar manner (though maybe use the "info" button rather than the detail disclosure):

iPad right callout accessory

But here you can have mapView:annotationView:calloutAccessoryControlTapped: actually generate the popover rather than doing a modal transition:

- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
//CGRect frame = view.frame;
[mapView deselectAnnotation:view.annotation animated:YES];

DetailsViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:@"DetailsPopover"];
controller.annotation = view.annotation;
self.popover = [[UIPopoverController alloc] initWithContentViewController:controller];
self.popover.delegate = self;
[self.popover presentPopoverFromRect:view.frame
inView:view.superview
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
}

which yields:

iPad popover

By the way, this roughly approximates how the iPad Maps app does it (when you click on a pin, it shows you a callout with an "info" button), which if you then click on the info button, it shows you the popover with details.

Alternatively, you could have the click on the pin take you right to your popover, bypassing the intervening callout. To do so, you first, though, have to disable the callout on the annotation view, itself:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
MKAnnotationView *annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"loc"];
annotationView.canShowCallout = NO;

return annotationView;
}

But you then have to respond to mapView:didSelectAnnotationView::

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
[mapView deselectAnnotation:view.annotation animated:YES];

DetailsViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:@"DetailsPopover"];
controller.annotation = view.annotation;
self.popover = [[UIPopoverController alloc] initWithContentViewController:controller];
self.popover.delegate = self;
[self.popover presentPopoverFromRect:view.frame
inView:view.superview
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
}

Theoretically, you could do something like this on the iPhone, but since you can't use UIPopoverController, you'd have to use some third-party popover (or write your own). I've heard some people allege that Apple has rejected iPhone apps for using popover views, though I can neither confirm that, nor say whether that's a hard and fast rule. I just know that the Apple and Google iPhone mapping apps do not use large popover views on the iPhone maps apps (Apple segues to another view, Google has it appear at the bottom of the screen). And if you think about it, if the pin is right in the center and you tried to generate a large popover pointing to that pin, it might get cramped.

Anyway, those are the options for using rightCalloutAccessoryView settings and/or disabling canShowCallout and showing the popover directly.



Parsing references:

  • Apple Event-Driven XML Programming Guide
  • NSXMLParser Class Reference

Map view callout reference:

  • Customizing the MKAnnotation Callout bubble

Parsing XML file with NSXMLParser - getting values

First you need to create an object that does the parsing. It will instatiate the NSXMLParser instance, set itself as the delegate for the parser and then call the parse message. It can also be responsible for storing your four result arrays:

NSXMLParser * parser = [[NSXMLParser alloc] initWithData:_data];
[parser setDelegate:self];
BOOL result = [parser parse];

The message you are most interested in implementing in your delegate objects is didStartElement. This guy gets called for each element in your XML file. In this callback you can add your name, price & where attributes to their respective arrays.

- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qualifiedName
attributes:(NSDictionary *)attributeDict
{
// just do this for item elements
if(![elementName isEqual:@"item"])
return;

// then you just need to grab each of your attributes
NSString * name = [attributeDict objectForKey:@"name"];

// ... get the other attributes

// when we have our various attributes, you can add them to your arrays
[m_NameArray addObject:name];

// ... same for the other arrays
}

NSXMLParser on iOS, how do I use it given a xml file

The simplest thing is to do something like this:

NSXMLParser *xmlParser = [[NSXMLParser alloc]initWithData:<yourNSData>];
[xmlParser setDelegate:self];
[xmlParser parse];

Notice that setDelegate: is setting the delegate to 'self', meaning the current object. So, in that object you need to implement the delegate methods you mention in the question.

so further down in your code, paste in:

    - (void) parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName 
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qualifiedName
attributes:(NSDictionary *)attributeDict{

NSLog(@"I just found a start tag for %@",elementName);
if ([elementName isEqualToString:@"employee"]){
// then the parser has just seen an <employee> opening tag
}
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
NSLog(@"the parser just found this text in a tag:%@",string);
}

etc. etc.

It's a little harder when you want to do something like set a variable to the value of some tag, but generally it's done using a class variable caleld something like "BOOL inEmployeeTag" which you set to true (YES) in the didStartElement: method and false in the didEndElement: method - and then check for it's value in the foundCharacters method. If it's yes, then you assign the var to the value of string, and if not you don't.

richard

NSXMLParser example

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict{
currentKey = nil;
[currentStringValue release];
currentStringValue = nil;
if([elementName isEqualToString:@"Value"]){
//alloc some object to parse value into
} else if([elementName isEqualToString:@"Signature"]){
currentKey = @"Signature";
return;
}
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
if(currentKey){
if(!currentStringValue){
currentStringValue = [[NSMutableString alloc] initWithCapacity:200];
}
[currentStringValue appendString:string];
}
}

-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{
if([elementName isEqualToString:@"Signature"] && [currentStringValue intValue] == 804){
ivar.signature = [currentStringValue intValue];
return;
}
}

Something like this.
Note I havent really tested this code on compiler so there will be syntax errors here & there.

How can i Parse this xml using NSXMLParser in ios?

Try this:

- (void)viewDidLoad {
[super viewDidLoad];

NSURL *url = [[NSURL alloc] initWithString:@"yourURL"];
NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
[parser setDelegate:self];
BOOL result = [parser parse];
// Do whatever with the result
}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict {
NSLog(@"Did start element");
if ([elementName isEqualToString:@"root"]) {
NSLog(@"found rootElement");
return;
}
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
NSLog(@"Did end element");
if ([elementName isEqualToString:@"root"]) {
NSLog(@"rootelement end");
}
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
NSString *tagName = @"column";
if ([tagName isEqualToString:@"column"]) {
NSLog(@"Value %@",string);
}
}

How to wait for NSXMLParser to complete in iOS

NSXMLParser works completely synchronous. When the parse method returns the parsing is finished (or has failed with an error). So there is no need to "wait" for it to complete.

Streaming NSXMLParser with NSInputStream

-[NSXMLParser initWithStream:] is the only interface to NSXMLParser that currently performs a streaming parse of the data. Hooking it up to an asynchronous NSURLConnection that's providing data incrementally is unwieldy because NSXMLParser takes a blocking, "pull"-based approach to reading from the NSInputStream. That is, -[NSXMLParser parse] does something like the following when dealing with an NSInputStream:

while (1) {
NSInteger length = [stream read:buffer maxLength:maxLength];
if (!length)
break;

// Parse data …
}

In order to incrementally provide data to this parser a custom NSInputStream subclass is needed that funnels data received by the NSURLConnectionDelegate calls on a background queue or runloop over to the -read:maxLength: call that NSXMLParser is waiting on.

A proof-of-concept implementation follows:

#include <Foundation/Foundation.h>

@interface ReceivedDataStream : NSInputStream <NSURLConnectionDelegate>
@property (retain) NSURLConnection *connection;
@property (retain) NSMutableArray *bufferedData;
@property (assign, getter=isFinished) BOOL finished;
@property (retain) dispatch_semaphore_t semaphore;
@end

@implementation ReceivedDataStream

- (id)initWithContentsOfURL:(NSURL *)url
{
if (!(self = [super init]))
return nil;

NSURLRequest *request = [NSURLRequest requestWithURL:url];
self.connection = [[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO] autorelease];
self.connection.delegateQueue = [[[NSOperationQueue alloc] init] autorelease];
self.bufferedData = [NSMutableArray array];
self.semaphore = dispatch_semaphore_create(0);

return self;
}

- (void)dealloc
{
self.connection = nil;
self.bufferedData = nil;
self.semaphore = nil;

[super dealloc];
}

- (BOOL)hasBufferedData
{
@synchronized (self) { return self.bufferedData.count > 0; }
}

#pragma mark - NSInputStream overrides

- (void)open
{
NSLog(@"open");
[self.connection start];
}

- (void)close
{
NSLog(@"close");
[self.connection cancel];
}

- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)maxLength
{
NSLog(@"read:%p maxLength:%ld", buffer, maxLength);
if (self.isFinished && !self.hasBufferedData)
return 0;

if (!self.hasBufferedData)
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);

NSAssert(self.isFinished || self.hasBufferedData, @"Was woken without new information");

if (self.isFinished && !self.hasBufferedData)
return 0;

NSData *data = nil;
@synchronized (self) {
data = [[self.bufferedData[0] retain] autorelease];
[self.bufferedData removeObjectAtIndex:0];
if (data.length > maxLength) {
NSData *remainingData = [NSData dataWithBytes:data.bytes + maxLength length:data.length - maxLength];
[self.bufferedData insertObject:remainingData atIndex:0];
}
}

NSUInteger copiedLength = MIN([data length], maxLength);
memcpy(buffer, [data bytes], copiedLength);
return copiedLength;
}

#pragma mark - NSURLConnetionDelegate methods

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(@"connection:%@ didReceiveData:…", connection);
@synchronized (self) {
[self.bufferedData addObject:data];
}
dispatch_semaphore_signal(self.semaphore);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(@"connectionDidFinishLoading:%@", connection);
self.finished = YES;
dispatch_semaphore_signal(self.semaphore);
}

@end

@interface ParserDelegate : NSObject <NSXMLParserDelegate>
@end

@implementation ParserDelegate

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict
{
NSLog(@"parser:%@ didStartElement:%@ namespaceURI:%@ qualifiedName:%@ attributes:%@", parser, elementName, namespaceURI, qualifiedName, attributeDict);
}

- (void)parserDidEndDocument:(NSXMLParser *)parser
{
NSLog(@"parserDidEndDocument:%@", parser);
CFRunLoopStop(CFRunLoopGetCurrent());
}

@end

int main(int argc, char **argv)
{
@autoreleasepool {

NSURL *url = [NSURL URLWithString:@"http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml"];
ReceivedDataStream *stream = [[ReceivedDataStream alloc] initWithContentsOfURL:url];
NSXMLParser *parser = [[NSXMLParser alloc] initWithStream:stream];
parser.delegate = [[[ParserDelegate alloc] init] autorelease];

[parser performSelector:@selector(parse) withObject:nil afterDelay:0.0];

CFRunLoopRun();

}
return 0;
}

Resolving html entities with NSXMLParser on iPhone

After exploring several alternatives, it appears that NSXMLParser will not support entities other than the standard entities <, >, ', " and &

The code below fails resulting in an NSXMLParserUndeclaredEntityError.


// Create a dictionary to hold the entities and NSString equivalents
// A complete list of entities and unicode values is described in the HTML DTD
// which is available for download http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent

NSDictionary *entityMap = [NSDictionary dictionaryWithObjectsAndKeys:
[NSString stringWithFormat:@"%C", 0x00E8], @"egrave",
[NSString stringWithFormat:@"%C", 0x00E0], @"agrave",
...
,nil];

NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
[parser setDelegate:self];
[parser setShouldResolveExternalEntities:YES];
[parser parse];

// NSXMLParser delegate method
- (NSData *)parser:(NSXMLParser *)parser resolveExternalEntityName:(NSString *)entityName systemID:(NSString *)systemID {
return [[entityMap objectForKey:entityName] dataUsingEncoding: NSUTF8StringEncoding];
}

Attempts to declare the entities by prepending the HTML document with ENTITY declarations will pass, however the expanded entities are not passed back to parser:foundCharacters and the è and à characters are dropped.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
[
<!ENTITY agrave "à">
<!ENTITY egrave "è">
]>

In another experiment, I created a completely valid xml document with an internal DTD

<?xml version="1.0" standalone="yes" ?>
<!DOCTYPE author [
<!ELEMENT author (#PCDATA)>
<!ENTITY js "Jo Smith">
]>
<author>< &js; ></author>

I implemented the parser:foundInternalEntityDeclarationWithName:value:; delegate method and it is clear that the parser is getting the entity data, however the parser:foundCharacters is only called for the pre-defined entities.

2010-03-20 12:53:59.871 xmlParsing[1012:207] Parser Did Start Document
2010-03-20 12:53:59.873 xmlParsing[1012:207] Parser foundElementDeclarationWithName: author model:
2010-03-20 12:53:59.873 xmlParsing[1012:207] Parser foundInternalEntityDeclarationWithName: js value: Jo Smith
2010-03-20 12:53:59.874 xmlParsing[1012:207] didStartElement: author type: (null)
2010-03-20 12:53:59.875 xmlParsing[1012:207] parser foundCharacters Before:
2010-03-20 12:53:59.875 xmlParsing[1012:207] parser foundCharacters After: <
2010-03-20 12:53:59.876 xmlParsing[1012:207] parser foundCharacters Before: <
2010-03-20 12:53:59.876 xmlParsing[1012:207] parser foundCharacters After: <
2010-03-20 12:53:59.877 xmlParsing[1012:207] parser foundCharacters Before: <
2010-03-20 12:53:59.878 xmlParsing[1012:207] parser foundCharacters After: <
2010-03-20 12:53:59.879 xmlParsing[1012:207] parser foundCharacters Before: <
2010-03-20 12:53:59.879 xmlParsing[1012:207] parser foundCharacters After: < >
2010-03-20 12:53:59.880 xmlParsing[1012:207] didEndElement: author with content: < >
2010-03-20 12:53:59.880 xmlParsing[1012:207] Parser Did End Document

I found a link to a tutorial on Using the SAX Interface of LibXML. The xmlSAXHandler that is used by NSXMLParser allows for a getEntity callback to be defined. After calling getEntity, the expansion of the entity is passed to the characters callback.

NSXMLParser is missing functionality here. What should happen is that the NSXMLParser or its delegate store the entity definitions and provide them to the xmlSAXHandler getEntity callback. This is clearly not happening. I will file a bug report.

In the meantime, the earlier answer of performing a string replacement is perfectly acceptable if your documents are small. Check out the SAX tutorial mentioned above along with the XMLPerformance sample app from Apple to see if implementing the libxml parser on your own is worthwhile.

This has been fun.



Related Topics



Leave a reply



Submit