Dzisiaj post o tym jak sparsować XMLa na iPhonie. Przykład będzie prosty by pokazać zasadę działania ;)  Ale za to będzie gotowa klasa do wykorzystania w dowolnym projekcie.

Wstęp

Ze względu na to, że poniższy post ma być tylko wstępem do parsowania, przykładowy XML jest krótki i ma formę jak poniżej:

<list>
	<link>http://sparhawk.pl</link>
	<link>http://twoja_strona.pl</link>
</list>

Od razu zaznaczam, że osoby które są przyzwyczajone do programowania w innych językach, będą musiały pozbyć się niektórych nawyków. Tu mamy coś co nazywa się Event-Driven XML Programing. Co to jest? To parsowanie XMLa w oparciu o zdarzenia, o których jesteśmy informowani i na nie reagujemy.

Plik nagłówkowy parsera

#import
#import "Parser.h"

@interface OrderListParser : NSObject {
    NSMutableString *_contentOfCurrentProperty;
    NSMutableArray  *_parameters;
    UIViewController *_controller;
}

@property (nonatomic, retain) NSMutableString *contentOfCurrentProperty;
@property (nonatomic, retain) NSMutableArray  *parameters;
@property (nonatomic, retain) UIViewController *controller;

- (void)parseXML:(NSMutableData *)data
        controller:(UIViewController *)contr
        parseError:(NSError **)error;

@end

W mojej klasie, którą wykorzystuje w jakimś kontrolerze zdefiniowałem parę property (o których później), a także metodę którą wykorzystuję, w kontrolerze do wywołania parsera i tak:

- (void)parseXML:(NSMutableData *)data
        controller:(UIViewController *)contr
        parseError:(NSError **)error;

Pierwszy parametr to treść XMLa do parsowania. Drugi to kontroler do, którego zwrócę wyniki. Ostatni można wykorzystać do przekazania parametrów błędów.

Implementacja parsera

Na początek implementacja metody tworzącej parser.

#import "ExampleListParser.h"
#import "ExampleListViewController.h"

@implementation ExampleListParser

@synthesize contentOfCurrentProperty = _contentOfCurrentProperty;
@synthesize parameters = _parameters;
@synthesize controller = _controller;

- (void)parseXML:(NSMutableData *)data controller:(UIViewController *)contr
                        parseError:(NSError **)error
{

Powołanie obiektu parsera do życia:

    NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];

Ustawienie obiektu, który będzie implementował metody delegujące:

    [parser setDelegate:delegator];

Poniższe nazwy metod mówią same za siebie, a jeżeli nie odsyłam do dokumentacji klasy NXSMLParser. Nasz XML jest na tyle prosty, że nie ma namespaców.

    [parser setShouldProcessNamespaces:NO];
    [parser setShouldReportNamespacePrefixes:NO];
    [parser setShouldResolveExternalEntities:NO];

Ustawienie kontrolera, do którego będą przekazane dane po sparsowaniu

    self.controller = contr;
    // parsowanie
    [parser parse];

Obsługa błędów parsera. Można ją rozbudować w zależności od potrzeb:

    NSError *parseError = [parser parserError];
    if (parseError && error) {
        *error = parseError;
    }

    // zwolnienie obiektu parsera
    [parser release];
}

Początek parsowania

Po uruchomieniu parsera zostaje nam tylko odpowiednio zaimplementować metody delegowane tak by odpowiednio tworzyły obiekty. Pierwszą jaką musimy zaimplementować jest reakcja na nowo napotkany węzeł:

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

Jeżeli otrzymaliśmy kwalifikowana nazwę to ją przyjmij jako podstawową:

    if (qName) {
        elementName = qName;
    }

Jeżeli trafisz na korzeń to stwórz tablicę dla linków:

    if ([elementName isEqualToString:@"list"]) {
        self.parameters = [NSMutableArray array];
        return;
    }

Jeżeli obecnie znalezionym elementem jest link, to przygotuj obiekt NSMutableString na treść linka

    if([elementName isEqualToString:@"link"]) {
        self.contentOfCurrentProperty = [NSMutableString string];
    } else {
        // w innym wypadku wyczyść zawartość
        self.contentOfCurrentProperty = nil;
    }

}

Opis parametrów metody:

  • parser – obiekt parsera
  • elementName – nazwa elementu w naszym xmlu to np. list, link
  • namespaceURI – gdy włączona jest obsługa namespace, tu przekazany jest URI obecnego namespace
  • qualifiedName – gdy włączona jest obsługa namespace, jest to nazwa kwalifikowana
  • attributeDict – gdy element ma atrybuty, przekazywane one są w postaci słownika (NSDictionary), gdzie klucz to nazwa atrybutu, a wartość to wartość atrybutu

Powyższa metoda będzie wywoływana za każdym razem, jak parser natrafi na znacznik otwierający. Dla zagłębionych elementów będziemy musieli tu odpowiednio tworzyć obiekty (tablice, słowniki, itp.).
Stworzenie tablicy dla linków można by też zaimplementować w metodzie, która jest wywoływana na początku parsowania: - (void)parserDidStartDocument:(NSXMLParser *)parser.

Trafiliśmy na znacznik zamykający

Gdy parser natrafia na znacznik zamykający wywołuje poniższą metodę:

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
                                     namespaceURI:(NSString *)namespaceURI
                                     qualifiedName:(NSString *)qName
{
    if (qName) {
        elementName = qName;
    }

Jeżeli trafiłem na link to dodaj go do tablicy, self.parameters. Treść linka jest uzupełniania przez metodę parser:foundCharacters:

    if([elementName isEqualToString:@"link"]) {
        [self.parameters addObject:self.contentOfCurrentProperty];
	return;
    }
}

Parametry powyższej metody tej analogiczne do parser:didStartElement.

Pozostałe metody

Zostały jeszcze dwie metody. Pierwsza to reakcja na znaleziony znak, a druga to zakończenie parsowania.

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    // jeżeli znalazłeś znak to dołącz go do obecnej zawartości znacznika
    if (self.contentOfCurrentProperty) {
        [self.contentOfCurrentProperty appendString:string];
    }
}

- (void)parserDidEndDocument:(NSXMLParser *)parser
{
    // wywolaj metodę didEndParse na kontrolerze wraz
    // ze sparsowanymi elementami
    [self.controller didEndParse:self.parameters];
}

W moim przykładzie, gdy skończy się parsowanie wywołuje metodę na kontrolerze, który sobie przekazałem wraz z treścią XMLa. Każdy może sobie zrobić tu inną dowolną akcję, w tym wsadzić Parser bezpośrednio do kontrolera. Ja wolałem mieć parser poza właściwym kontrolerem.

Ostateczna implementacja

Zamieszczam ostateczny kształt pliku .m

#import "ExampleListParser.h"
#import "ExampleListViewController.h"

@implementation ExampleListParser

@synthesize contentOfCurrentProperty = _contentOfCurrentProperty;
@synthesize parameters = _parameters;
@synthesize controller = _controller;

- (void)parseXML:(NSMutableData *)data controller:(UIViewController *)contr
                                        parseError:(NSError **)error
{
	NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
    [parser setDelegate:delegator];
    [parser setShouldProcessNamespaces:NO];
    [parser setShouldReportNamespacePrefixes:NO];
    [parser setShouldResolveExternalEntities:NO];
    self.controller = contr;
    [parser parse];

    NSError *parseError = [parser parserError];
    if (parseError && error) {
        *error = parseError;
    }

    [parser release];
}

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

    if (qName) {
        elementName = qName;
    }

    if ([elementName isEqualToString:@"list"]) {
        self.parameters = [NSMutableArray array];
        return;
    }

	if([elementName isEqualToString:@"link"]) {
		self.contentOfCurrentProperty = [NSMutableString string];
	} else {
        self.contentOfCurrentProperty = nil;
    }
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
                                     namespaceURI:(NSString *)namespaceURI
                                     qualifiedName:(NSString *)qName
{
    if (qName) {
        elementName = qName;
    }

	if([elementName isEqualToString:@"link"]) {
		[self.parameters addObject:self.contenOfCurrentProperty];
		return;
	}
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    if (self.contentOfCurrentProperty) {
        [self.contentOfCurrentProperty appendString:string];
    }
}

- (void)parserDidEndDocument:(NSXMLParser *)parser
{
	[self.controller didEndParse:self.parameters];
}

@end

Przykład w postaci plików

Podsumowanie

Parsowanie XMLa na iPhonie to typowe Event-Driven Development wg. Apple ;) Ma to swoje zalety i wady. Do zalet można zaliczyć, że już na etapie parsowania, można odrzucać nie interesujące nas dane lub je wstępnie przetwarzać. Wady to trochę inne podejście rozrost ilości metod.
Ostatnia uwaga pamiętaj, że na telefonie parsowanie może przy dużych XMLach chwilę trwać i zżera zasoby;)

Linki z dalszymi informacjami

  1. Tutorial od Apple
  2. Opis wykorzystywanej klasy parsera