Dev (Back & Front)ARTIGO

Usando NSXML Parser

Esta é minha
primeira apunhalada em um projeto do iPhone. Estou escrevendo um aplicativo que
consome um RSS feed e o analisa para recuperar alguns bits de informação para
exibir na tela. Esta é minha primeira tentativa de implementação. Vindo de um
background .NET, é uma grande mudança de paradigma migrar para o
mundo fortemente baseado em convenções do desenvolvimento em Cocoa.

Se eu estivesse escrevendo esse aplicativo em .NET, iria procurar pelas bibliotecas corretas de classes para analisar o
XML e para começar a enquadrar uma classe para executar todas as funções que eu
precisasse para popular um objeto que fosse persistente à interface do usuário.

Ao pesquisar pela melhor maneira de fazer isso, eu
encontrei dois métodos para analisar o XML no Cocoa:

Eu decidi usar o NSXMLParser
porque ele parecia ser o caminho com menos obstáculos para começar. O
NSXMLParser é um leitor baseado em stream. Ele basicamente lê toda a árvore XML caractere por caractere, e dispara eventos a partir de chamados delegados
quando encontra informações úteis. Eu achei esse modelo muito fácil de usar.
Depois de algumas tentativas e erros, eu consegui fazê-lo funcionar bem para
meu projeto atual.

Estou analisando o feed xml para o Woot.com. O código a seguir é um fragmento
dos elementos em que estou interessado. O feed real tem muito mais informações,
mas isto deve te dar uma idéia da estrutura:

<channel>
<copyright>
<description>Woot! - One Day, One Deal</description>
<docs>http://www.rssboard.org/rss-specification</docs>
<generator>Oppositional.Syndication, 1.0.0.0, http://www.oppositionallydefiant.com/</generator>
<item>
<description>[Removed for brevity]</description>
<link>http://www.woot.com/
<title>Dyson DC24 Blueprint Limited Edition</title>
<woot:price xmlns:woot="http://www.woot.com/">$279.99</woot:price>
<woot:soldout xmlns:woot="http://www.woot.com/">False</woot:soldout>
<woot:wootoff xmlns:woot="http://www.woot.com/">False</woot:wootoff>
<woot:purchaseurl xmlns:woot="http://www.woot.com/">http://www.woot.com/WantOne.aspx?id=e94aa72d-5860-4f9b-bab2-42211de2f4cc</woot:purchaseurl>
<woot:thumbnailimage xmlns:woot="http://www.woot.com/">http://sale.images.woot.com/Dyson_DC24_Blueprint_Limited_EditionbklThumbnail.jpg</woot:thumbnailimage>
<woot:substandardimage xmlns:woot="http://www.woot.com/">http://sale.images.woot.com/Dyson_DC24_Blueprint_Limited_EditionxxcShoppingYahoo.jpg</woot:substandardimage>
<woot:standardimage xmlns:woot="http://www.woot.com/">http://sale.images.woot.com/Dyson_DC24_Blueprint_Limited_Editionm32Standard.jpg</woot:standardimage>
<woot:detailimage xmlns:woot="http://www.woot.com/">http://sale.images.woot.com/Dyson_DC24_Blueprint_Limited_Edition98lDetail.jpg</woot:detailimage>
<woot:shipping xmlns:woot="http://www.woot.com/">$5 shipping</woot:shipping>
</item>
</copyright></channel>

Definindo o objeto modelo

A
primeira coisa que eu fiz foi criar um objeto modelo que iria encapsular os valores do elemento a partir do RSS feed em que eu estava interessado. Aqui está o
código para a classe:

SaleItem.h

#import <foundation foundation.h="">


@interface SaleItem : NSObject {
NSString *itemTitle;
NSString *itemPrice;
NSString *itemThumbnaillUrl;
NSString *itemSubstandardImageUrl;
NSString *itemStandardImageUrl;
NSString *itemDetailImageUrl;
NSString *itemDescription;
NSString *itemShippingCost;
NSString *itemSaleUrl;

BOOL isWootOff;
}

@property (nonatomic, retain) NSString *itemTitle;
@property (nonatomic, retain) NSString *itemPrice;
@property (nonatomic, retain) NSString *itemThumbnaillUrl;
@property (nonatomic, retain) NSString *itemSubstandardImageUrl;
@property (nonatomic, retain) NSString *itemStandardImageUrl;
@property (nonatomic, retain) NSString *itemDetailImageUrl;
@property (nonatomic, retain) NSString *itemDescription;
@property (nonatomic, retain) NSString *itemShippingCost;
@property (nonatomic, retain) NSString *itemSaleUrl;

@property (nonatomic, assign) BOOL isWootOff;

@end
</foundation>

SaleItem.m

#import "SaleItem.h"


@implementation SaleItem

@synthesize itemTitle;
@synthesize itemPrice;
@synthesize itemThumbnaillUrl;
@synthesize itemSubstandardImageUrl;
@synthesize itemStandardImageUrl;
@synthesize itemDetailImageUrl;
@synthesize itemDescription;
@synthesize itemShippingCost;
@synthesize itemSaleUrl;
@synthesize isWootOff;


-(void) dealloc{
[itemTitle release];
[itemPrice release];
[itemThumbnaillUrl release];
[itemSubstandardImageUrl release];
[itemStandardImageUrl release];
[itemDetailImageUrl release];
[itemDescription release];
[itemShippingCost release];
[itemSaleUrl release];

[super dealloc];
}

@end

Definindo uma classe de referências constantes

Em seguida, para facilitar a minha vida, eu
defini uma classe para definir algumas constantes. Essas constantes definem os
valores string que eu estarei procurando ao analisar o XML.

Constants.h

#import <foundation foundation.h="">

extern NSString * const ITEM;
extern NSString * const TITLE;
extern NSString * const PRICE;
extern NSString * const THUMBNAIL_IMAGE;
extern NSString * const SUBSTANDARD_IMAGE;
extern NSString * const STANDARD_IMAGE;
extern NSString * const DETAIL_IMAGE;
extern NSString * const DESCRIPTION;
extern NSString * const IS_WOOTOFF;
extern NSString * const SHIPPING_PRICE;
extern NSString * const SALE_URL;

@interface Constants : NSObject {

}
</foundation>

Constants.m

#import "Constants.h"
@implementation Constants
NSString * const ITEM = @"item";
NSString * const TITLE = @"title";
NSString * const PRICE = @"woot:price";
NSString * const THUMBNAIL_IMAGE = @"woot:thumbnailimage";
NSString * const SUBSTANDARD_IMAGE = @"woot:substandardimage";
NSString * const STANDARD_IMAGE = @"woot:standardimage";
NSString * const DETAIL_IMAGE = @"woot:detailimage";
NSString * const DESCRIPTION = @"description";
NSString * const IS_WOOTOFF = @"woot:wootoff";
NSString * const SHIPPING_PRICE = @"woot:shipping";
NSString * const SALE_URL = @"woot:purchaseurl";
@end

Criando uma classe Worker

O NSXMLParser
funciona delegando callbacks para deixar você ligado nos eventos em que está interessado. Muitos dos exemplos que encontrei online usam métodos
delegados dentro do viewcontroller para conseguir a análise. Como eu tenho
muitas visualizações que irão tirar vantagem de sua funcionalidade, decidi
dividi-las em uma classe worker. Vamos construir essa classe (eu a nomeei
XMLParser) peça por peça:

XMLParser.h

#import <uikit uikit.h="">

@class Constants; //Forward declaration of the Constants class
@class SaleItem; //Forward declaration of the SaleItem model object class

@interface XMLParser : NSObject {
NSMutableString *currentValue; //This will hold element values as we build them piece by piece
BOOL itemElementInProgress; // This will determine if we are currently parsing an element
SaleItem *saleItem; // Instance of our model object
}

@property (nonatomic, retain) NSMutableString *currentValue;
@property (nonatomic, retain) SaleItem *saleItem;
@property BOOL itemElementInProgress;

- (BOOL) parse; // This performs the parsing of the file
- (id)initWithSaleItem:(SaleItem *)aSaleItem; // Custom initializer
- (void) trimString; // Small convenience method to remove whitespace

@end
</uikit>

Construindo XMLParser.m

Vamos começar nossas primeiras linhas:

#import "Constants.h"
#import "SaleItem.h"
#import "XMLParser.h"

@implementation XMLParser
// Synthesize our properties
@synthesize currentValue;
@synthesize itemElementInProgress;
@synthesize saleItem;

// Make sure objects are released
-(void)dealloc{
[currentValue release];
[saleItem release];
[super dealloc];
}

@end

Agora vamos criar o inicializador. Adicione o código a seguir logo depois da chamada da propriedade
@synthesize:

// Sets the saleItem property to a SaleItem object passed in 
- (id)initWithSaleItem:(SaleItem *)aSaleItem{
if (self = [super init]) {
[self setSaleItem:aSaleItem];
[self setItemElementInProgress:NO];
}

return self;
}

Estou iniciando a classe dessa maneira para que eu
possa popular o SaleItem dentro do XMLParser e ainda ter acesso a ele a partir
da classe de chamado (um ViewController) quando eu tiver acabado de analisá-lo.

Antes de entrar em como instanciar o NSXMLParser e
o utilizar para analisar um arquivo, vamos configurar os métodos de delegação
para pegarmos os dados que precisamos de dentro do arquivo.

Eu normalmente coloco os métodos de delegação na
parte final do arquivo, logo depois da definição #pragma mark para distinguir esse grupo
de métodos do resto do código. Vá ao final do XMLParser.m, e cole
este código logo antes do @end:

#pragma mark NSXMLParser Delegate Calls
// This method gets called every time NSXMLParser encounters a new element
-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qualifiedName
attributes:(NSDictionary *)attributeDict{
// If element is named "item", set bool to true so we know
// we are inside the element in other methods. This is needed
// because "item" contains most of the data we need
if ([elementName isEqualToString:ITEM]) {
[self setItemElementInProgress:YES];
}
}

// This method gets called for every character NSXMLParser finds.
-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
// If currentValue doesn't exist, initialize and allocate
if (!currentValue) {
currentValue = [[NSMutableString alloc] init];
}

// Append the current character value to the running string
// that is being parsed
[currentValue appendString:string];
}

O método de delegação final é disparado quando o
NSXMLParser chega ao fim de um elemento. Aqui é o lugar em que podemos ter
certeza de que temos o valor completo do elemento, e podemos persistir esses
dados para nosso objeto modelo. Logo depois do método foundCharacters, adicione
este método:

// This method is called whenever NSXMLParser reaches the end of an element
-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName{
[self trimString]; // Remove whitespace with a convenience method

if ([elementName isEqualToString:ITEM]) {
[self setItemElementInProgress:NO]; // If we are currently on the "item" element, then do not save value
}


// If we are processing an element inside of "item",
// then determine if we need to save the value
if (self.itemElementInProgress) {
// Now check the element id against the ones we are
// interested in (using the Constants class values).
// If we find one, set the corresponding property on the
// SaleItem object
if ([elementName isEqualToString:TITLE]) {
[[self saleItem] setItemTitle:currentValue];
}

if ([elementName isEqualToString:PRICE]) {
[[self saleItem] setItemPrice:currentValue];
}

if ([elementName isEqualToString:THUMBNAIL_IMAGE]) {
[[self saleItem] setItemThumbnaillUrl:currentValue];
}

if ([elementName isEqualToString:SUBSTANDARD_IMAGE]) {
[[self saleItem] setItemSubstandardImageUrl:currentValue];
}

if ([elementName isEqualToString:STANDARD_IMAGE]) {
[[self saleItem] setItemStandardImageUrl:currentValue];
}

if ([elementName isEqualToString:DETAIL_IMAGE]) {
[[self saleItem] setItemDetailImageUrl:currentValue];
}

if ([elementName isEqualToString:DESCRIPTION]) {
[[self saleItem] setItemDescription:currentValue];
}

if ([elementName isEqualToString:SHIPPING_PRICE]) {
[[self saleItem] setItemShippingCost:currentValue];
}

if ([elementName isEqualToString:SALE_URL]) {
[[self saleItem] setItemSaleUrl:currentValue];
}
}

[currentValue release];
currentValue = nil;
}

O método Parse

Finalmente,
todo o processo é iniciado pelo método parse:

- (BOOL)parse{
// Create and initialize an NSURL with the RSS feed address and use it to instantiate NSXMLParser
NSURL *url = [[NSURL alloc] initWithString:@"http://www.woot.com/salerss.aspx"];
NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url];

// Tell NSXMLParser that this class is its delegate
[parser setDelegate:self];

// Kick off file parsing
[parser parse];

// Clean up
[url release];

[parser setDelegate:nil];
[parser release];
return YES;
}

Notas Finais

Alguns pensamentos finais sobre a metodologia
que usei:

  • Quero me livrar dos if statements e usar códigos Key-Value para
    configurar as propriedades de valor. Eu ainda não tive chance de refatorá-los.
  • Eu sou novo no Cocoa, e qualquer sugestão/comentário é bem-vindo!

?

Texto
original em inglês de Mark Struzinski, disponível em http://markstruzinski.com/main/2011/6/7/using-nsxmlparser.html

é programador desde 2003. Trabalhou inicialmente com tecnologias da Microsoft, e hoje trabalha com a plataforma iOS.

Ver perfil