죽어도  다시한번


Introdution
이 문서에서는 XML파일을 파싱하기 위해 NSXMLParser와 Delegate를 사용한 예제로써 활용할 수 있겠습니다.

NSXMLParser is a forward only reader or an event driven parser.
NSXMLParser는 오직 리더기로서 전송하거나 event driven 파서입니다.
이벤트란것은 파서가 XML 구성요소의 시작.. 즉 엘리먼트, value, CDATA 등의 첫부분을 만났을때 어디서나 발생합니다.
NSXMLPaser의 delegate는 XML 데이터를 잡아내는 이벤트들의 도구라고 할 수 있습니다. 어떤 이벤트들은 엘리먼트의 시작, 엘리먼트의 값들 처럼 다중으로 발생되기도 합니다.
iPhone은 NSXMLParser만 지원하고 메모리에 XML 트리로 전부 로드하는 NSXMLDocument는 지원하지 않습니다.
(이부분에서 전에 어느 사이트에 NSXMLDocument를 지원했지만 SDK가 버전업 되면서 바뀌었다고 합니다.)

'Books' Application
NSXMLParser의 사용법을 알기위해 간단한 'Navigation-based Application'을 생성해 봅니다.
이 예제는 테이블 뷰에 책 제목 리스트들을 보여주고, 그 리스트 중 하나를 선택하면 디테일뷰에 상세한 내용을 보여주는 예제입니다.
이 예제에 사용되는 XML file을 보시려면 <여기>를 클릭하세요.

그럼 XCode에서 Create a new Xcode project를 선택하고 Navigation-based Applicatoin을 선택합니다.
프로젝트 이름을 my app XML로 지으셨네요. 전 MyAppXML 로 합니다.
Since the NSXMLParser is a forward only parser or an event driven parser, we need to store the data locally, which can be used later.
XML 파일에서 반복되는 엘리먼트들과 속성들의 데이터를 저장하기 위해 클래스를 만들겁니다. 이 클래스의 인스턴스는 XML 파일에서 한개의 'Book' 엘리먼트를 나타냅니다. 여기서 이분께선 이클래스의 이름을 "Book" 이라고 이름지으셨네요... 아래는 소스코드 입니다.

//Book.h
#import <UIKit/UIKit.h>

@interface Book : NSObject {

NSInteger bookID;
NSString *title; //Same name as the Entity Name.
NSString *author; //Same name as the Entity Name.
NSString *summary; //Same name as the Entity Name.

}

@property (nonatomic, readwrite) NSInteger bookID;
@property (nonatomic, retain) NSString *title;
@property (nonatomic, retain) NSString *author;
@property (nonatomic, retain) NSString *summary;

@end

//Book.m
#import "Book.h"

@implementation Book

@synthesize title, author, summary, bookID;

- (void) dealloc {

[summary release];
[author release];
[title release];
[super dealloc];
}

@end


property 이름은 XML 파일에서 엘리먼트의 이름과 같습니다. XML파일에 'Book' 엘리먼트들이 n개가 있으며 이것들을 저장하기 위해 application delegate 에 배열을 선언합니다. 아래는 배열을 선언한 소스코드 입니다.

//XMLAppDelegate.h
#import <UIKit/UIKit.h>

@interface XMLAppDelegate : NSObject <UIApplicationDelegate> {

UIWindow *window;
UINavigationController *navigationController;

NSMutableArray *books;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;

@property (nonatomic, retain) NSMutableArray *books;

@end


이 예제에선 XMLAppDelegate.h 라고 되어 있지만 프로젝트를 만들때 프로젝트 이름으로 되어 있을겁니다.
저의 경우 MyAppXML로 생성해서 MyAppXMLAppDelegate.h 로 되어 있습니다.

Delegate
소스코드를 깔끔히 하기위해 NSXMLParser의 인스턴트에 사용할 수 있도록 delegate 역시 선언해 줍니다.

//XMLParser.h
#import <UIKit/UIKit.h>

@class XMLAppDelegate, Book;

@interface XMLParser : NSObject {

NSMutableString *currentElementValue;

XMLAppDelegate *appDelegate;
Book *aBook;
}

- (XMLParser *) initXMLParser;

@end


currentElementValue 는 엘리먼트의 값들을 위한것이고, appDelegate 는 book리스트를 저장하는 배열에 접근할 수 있으며, 마지막으로 aBook은 Book 클래스를 참조합니다.
Notice that we do not keep track of the current element name being processed, because the event will tell us that.
마지막으로 initXMLParser를 호출하는 생성자를 구현합니다.

//XMLParser.m
- (XMLParser *) initXMLParser {

[super init];

appDelegate = (XMLAppDelegate *)[[UIApplication sharedApplication] delegate];

return self;
}


application delete의 레퍼런스를 얻어서 그걸 리턴하는 매우 단순한 구조로 되어 있습니다.

XML파일 파싱
이제 모든 준비가 되었고 XML파일을 읽을 수 있는 코드입니다.

//XMLAppDelegate.m
- (void)applicationDidFinishLaunching:(UIApplication *)application {


NSURL *url = [[NSURL alloc] initWithString:@"http://sites.google.com/site/iphonesdktutorials/xml/Books.xml"];
NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:url];

//Initialize the delegate.
XMLParser *parser = [[XMLParser alloc] initXMLParser];

//Set delegate
[xmlParser setDelegate:parser];

//Start parsing the XML file.
BOOL success = [xmlParser parse];

if(success)
NSLog(@"No Errors");
else
NSLog(@"Error Error Error!!!");

// Configure and show the window
[window addSubview:[navigationController view]];
[window makeKeyAndVisible];
}


코드가 매우 간단한것이, NSURL 인스턴트를 생성하고, NSXMLParser 인스턴트를 생성하고, delegate를 초기화하고, delegate를 assign 하고, 파싱을 시작합니다. 파싱에 성공했다면 YES를 리턴하고, 에러가 났거나 중단됐다면 NO를 리턴합니다.

Parsing the start of an element(엘리먼트의 시작 파싱)
파서의 델리게이트는
파서가 문서를 읽기 시작할 때 그 이벤트를 처리하길 원하지 않을경우, 구현하지 않고 무시할 수 있습니다.
파서가 엘리먼트의 시작, 끝, 또는 값을 만났을때 호출되는 3개의 메소드만을 구현할 것입니다.

그럼 파서가 엘리먼트의 시작을 만났을때 호출되는 paser:didStartElement:namespaceURI:qualifiedName:attributes 메소드를 보시겠습니다.

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

if([elementName isEqualToString:@"Books"]) {
//Initialize the array.
appDelegate.books = [[NSMutableArray alloc] init];
}
else if([elementName isEqualToString:@"Book"]) {

//Initialize the book.
aBook = [[Book alloc] init];

//Extract the attribute here.
aBook.bookID = [[attributeDict objectForKey:@"id"] integerValue];

NSLog(@"Reading id value :%i", aBook.bookID);
}

NSLog(@"Processing Element: %@", elementName);
}


위 코드에서 첫번째로 "Books" 엘리먼트를 만났을때 배열을 초기화 합니다.
"Book" 엘리먼트를 만났다면 로컬 book 오브젝트를 초기화 하고, dictionary 오브젝트 속성(attribute)으로부터 최근 XML의 book 엘리먼트의 속성(attribute)를 읽습니다.

Parsing an element's value (엘리먼트들의 값<value> 파싱)
이제 XML 트리에서 현재 book 엘리먼트를 나타내는 로컬 book 오브젝트가 있고, 그다음엔 XML 데이터를 로컬 오브젝트에 담는것입니다. 파서는 이제 title 엘리먼트로 이동하고 같은 메소드가 다시한번 호출됩니다. 하지만 이시간엔 아무것도 하지 않습니다. 파서는 엘리먼트 값(value)로 이동하고 이것을 delegate에게 parser:foundCharacters 이벤트를 던집니다.

//XMLParser.m
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {

if(!currentElementValue)
currentElementValue = [[NSMutableString alloc] initWithString:string];
else
[currentElementValue appendString:string];

NSLog(@"Processing Value: %@", currentElementValue);

}


코드가 매우 쉽습니다. MutableString 이 nil 이면 파라미터로 넘어온 'string'으로 초기화를 해줍니다. 만약 currentElementValue가 nil이 아니면 단순히 'string' 값으로 데이터를 append 시킵니다.

Parsing the end of an element(엘리먼트의 마지막<end> 파싱)
파서는 이제 엘리먼트의 끝(end)으로 이동하고, parser:didEndElement:namespaceURI:qualifiedName가 delegate에게 보내지게 됩니다. 로컬 book 오브젝트의 정확한 프로퍼티에 currentElementValue를 셋팅하고 nil로 만듭니다.

//XMLParser.m
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {

if([elementName isEqualToString:@"Books"])
return;

//There is nothing to do if we encounter the Books element here.
//If we encounter the Book element howevere, we want to add the book object to the array
// and release the object.
if([elementName isEqualToString:@"Book"]) {
[appDelegate.books addObject:aBook];

[aBook release];
aBook = nil;
}
else
[aBook setValue:currentElementValue forKey:elementName];

[currentElementValue release];
currentElementValue = nil;
}


엘리먼트가 "Books"를 만나면 파일을 읽을때 거의 그랬던 것처럼 아무것도 하지 않습니다. 엘리먼트 이름이 "Book"이면 배열에 book 오브젝트를 추가하고 로컬 bool 오브젝트를 nil로 셋팅하고 메모리에서 해제하여 다시 사용할 수 있도록 합니다. 끝(end) 엘리먼트가 "Books"나 "Book"이 아니면 "book"의 서브 엘리먼트의 하나가 될것이고 setValue:forKey를 이용하여 현재 book 프로퍼티로 currentElementValue를 셋팅합니다.
이렇게 할 수 있는 이유는 book에 선언했던 property 들이 XML 엘리먼트 이름들과 같기 때문입니다.(실제로 이름이 같아야 파서가 알아서 파싱해주더군요)

book 오브젝트를 초기화 하고, 속석(attribute)를 읽고, 자식 엘리먼트들을 읽고, 로컬 오브젝트에 그 값들을 셋팅하고, 마지막으로 배열에 오브젝트를 추가하는 이런 사이클이 다시 시작됩니다. 파서는 eof를 만나기 전까지 세개의 함수(function)를 반복해서 호출합니다.

밑에는 XMLParser.m 파일의 완성된 코드입니다.

//XMLParser.m
#import "XMLParser.h"
#import "XMLAppDelegate.h"
#import "Book.h"

@implementation XMLParser

- (XMLParser *) initXMLParser {

[super init];

appDelegate = (XMLAppDelegate *)[[UIApplication sharedApplication] delegate];

return self;
}

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

if([elementName isEqualToString:@"Books"]) {
//Initialize the array.
appDelegate.books = [[NSMutableArray alloc] init];
}
else if([elementName isEqualToString:@"Book"]) {

//Initialize the book.
aBook = [[Book alloc] init];

//Extract the attribute here.
aBook.bookID = [[attributeDict objectForKey:@"id"] integerValue];

NSLog(@"Reading id value :%i", aBook.bookID);
}

NSLog(@"Processing Element: %@", elementName);
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {

if(!currentElementValue)
currentElementValue = [[NSMutableString alloc] initWithString:string];
else
[currentElementValue appendString:string];

NSLog(@"Processing Value: %@", currentElementValue);

}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {

if([elementName isEqualToString:@"Books"])
return;

//There is nothing to do if we encounter the Books element here.
//If we encounter the Book element howevere, we want to add the book object to the array
// and release the object.
if([elementName isEqualToString:@"Book"]) {
[appDelegate.books addObject:aBook];

[aBook release];
aBook = nil;
}
else
[aBook setValue:currentElementValue forKey:elementName];

[currentElementValue release];
currentElementValue = nil;
}

- (void) dealloc {

[aBook release];
[currentElementValue release];
[super dealloc];
}

@end


아래는 table view와 detail view 콘트롤러에서 데이터가 어떻게 보여지는지 확인할 수 있습니다.



다운로드

'Xcode > Examples' 카테고리의 다른 글

xcode 4.2 Navigation Controller exam  (0) 2011.10.13
UITableView  (0) 2011.07.24