XML 데이터를 NSArray와 NSDictionary로 구조화하자.
서버 연동 앱을 개발하다 보면 서버로 부터 XML을 받아 파싱하여 처리하는 경우가 많습니다.
최근에는 JSON을 많이 쓰긴 합니다만 자바로 구축된 시스템은 여전히 XML을 많이 사용합니다.
이 때 파싱된 결과를 NSArray와 NSDictionary로 구조화하여 쉽게 핸들링할 수 있는 방법을 소개하고자 합니다.
JSON의 경우 파싱모듈 자체에 배열과 딕셔너리로 구조화해주는 오픈소스가 제공됩니다만
xml은 약간의 구글링으로 직접 구현해야 합니다.
아레의 XML형식으로 된 연락처 데이터가 있다고 가정해 봅시다.
<?xml version="1.0" encoding="utf-8" ?>
<Contacts> <contact> <name>둘리</name> <address>서울 여의도</address> <phones> <phone>010-1111-1111</phone> <phone>010-2222-2222</phone> </phones> <emails> <email>aaaa@me.com</email> <email>aaaa@gmail.com</email> </emails> </contact> <contact> <name>희동이</name> <address>서울 신림동</address> <phones> <phone>010-3333-3333</phone> <phone>010-4444-4444</phone> </phones> <emails> <email>bbbb@me.com</email> <email>bbbb@gmail.com</email> </emails> </contact> <contact> <name>또치</name> <address>서울 구로동</address> <phones> <phone>010-5555-5555</phone> <phone>010-6666-6666</phone> </phones> <emails> <email>cccc@me.com</email> <email>cccc@gmail.com</email> </emails> </contact> <contact> <name>도우너</name> <address>경기도 분당</address> <phones> <phone>010-7777-7777</phone> <phone>010-8888-8888</phone> </phones> <emails> <email>dddd@me.com</email> <email>dddd@gmail.com</email> </emails> </contact> <contact> <name>마이콜</name> <address>서울 서초동</address> <phones> <phone>010-9999-9999</phone> </phones> <emails> <email>eeee@me.com</email> </emails> </contact> </Contacts> |
위의 데이터를 파싱하고 NSArray와 NSDictionary로 구조화하기 위해 XMLController라는 클래스를 만들었습니다.
핵심구현 부분은 아래와 같습니다.
헤더
@interface XMLController : NSObject <NSXMLParserDelegate>
{ NSMutableString *stringXMLParsing; // 파싱중인 xml 데이터 문자
NSMutableArray *dictionaryStack; NSMutableDictionary *dicRoot; // 루트 딕셔너리 } @property (nonatomic, retain) NSMutableString *stringXMLParsing; @property (nonatomic, readonly) NSMutableDictionary *dicRoot; // 파싱 - (BOOL)parseXML:(NSString *)xmlString; // 주어진 태그의 문자열을 가져 온다. - (NSString *)stringWithKey:(NSString *)sKey; + (NSString *)stringWithKey:(NSString *)sKey fromDic:(NSDictionary *)dic; // 주어진 태그의 배열을 가져 온다 - (NSArray *)arrayWithKey:(NSString *)sKey; + (NSArray *)arrayWithKey:(NSString *)sKey fromDic:(NSDictionary *)dic; @end |
핵심구현 부
- (BOOL)parseXML:(NSString *)xmlString
{ self.stringXMLParsing = nil; [dictionaryStack release]; dicRoot = nil;
dictionaryStack = [[NSMutableArray alloc] init]; [dictionaryStack addObject:[NSMutableDictionary dictionary]];
// xml 파싱 NSXMLParser *parser = [[NSXMLParser alloc] initWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding]]; parser.delegate = self; parser.shouldResolveExternalEntities = YES; BOOL bRet = [parser parse]; [parser release];
if(bRet) { dicRoot = [dictionaryStack objectAtIndex:0]; NSArray *array = [dicRoot allValues]; if(array && [array count] == 1) // xml 루트노드는 항상 하나여야 한다. dicRoot = [array objectAtIndex:0]; }
NSLog(@"%@", dicRoot);
return bRet; } - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { if(!self.stringXMLParsing) self.stringXMLParsing = [NSMutableString stringWithCapacity:50]; [self.stringXMLParsing appendString:string]; } - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { // xml 엘리먼트 파싱 시작 시점에서 관련 객체 (배열 또는 딕셔너리)를 준비한다.
NSMutableDictionary *parentDict = [dictionaryStack lastObject]; NSMutableDictionary *childDict = [NSMutableDictionary dictionary]; [childDict addEntriesFromDictionary:attributeDict];
id existingValue = [parentDict objectForKey:elementName]; if (existingValue) { // 같은 태그 이름으로 이미 존재 한다면 배열로 넣어야 한다. NSMutableArray *array = nil; if ([existingValue isKindOfClass:[NSMutableArray class]]) { array = (NSMutableArray *)existingValue; } else { // 같은 태그가 이미 존재하는데 배열이 아니라면 배열로 전환 array = [NSMutableArray array]; [array addObject:existingValue]; [parentDict setObject:array forKey:elementName]; }
[array addObject:childDict]; } else { [parentDict setObject:childDict forKey:elementName]; }
// 현재 파싱대상 객체를 핸드링 하기 위한 임시 저장 [dictionaryStack addObject:childDict];
self.stringXMLParsing = nil; } - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { // xml 엘리먼트 파싱 종료 시점에서 파싱된 문자열을 text라는 키로 딕셔너리에 저장한다.
NSMutableDictionary *dictInProgress = [dictionaryStack lastObject];
if ([stringXMLParsing length] > 0) [dictInProgress setObject:stringXMLParsing forKey:@"text"]; // 현재 파싱대상 객체를 핸드링 하기 위해 임시 저장되었던 것 제거 [dictionaryStack removeLastObject];
self.stringXMLParsing = nil; } - (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError { self.stringXMLParsing = nil; } |
이렇게 구조화하면 아래와 같은 코드로 간단하게 연락처리스트를 테이블로 표시할 수 있습니다.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView
{ return 1; } - (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section { NSArray *arr = [xmlController arrayWithKey:@"contact"]; return [arr count]; } - (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { int row = indexPath.row; NSString *CellIdentifier = [NSString stringWithFormat:@"ContactNameCellController"]; UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; }
NSArray *arr = [xmlController arrayWithKey:@"contact"]; NSDictionary *dic = [arr objectAtIndex:row]; cell.textLabel.text = [XMLController stringWithKey:@"name" fromDic:dic];
return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES];
int row = indexPath.row; NSArray *arr = [xmlController arrayWithKey:@"contact"]; NSDictionary *dic = [arr objectAtIndex:row];
ContactInfoView *civ = [[[ContactInfoView alloc] init] autorelease]; civ.xmlController = xmlController; civ.contact = dic; [self.navigationController pushViewController:civ animated:YES]; } |
그리고 해당 연락처를 터치했을 때 세부정보를 보여주는 화면에서도 아래와 같이 간단한 코드로 구현이 가능합니다.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{ return 3; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { if(section == 1) return @"전화번호"; else if(section == 2) return @"이메일"; return @""; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if(section == 0) { return 2; } else if(section == 1) { NSDictionary *dic = [contact objectForKey:@"phones"]; NSArray *arr = [XMLController arrayWithKey:@"phone" fromDic:dic]; return [arr count]; } else if(section == 2) { NSDictionary *dic = [contact objectForKey:@"emails"]; NSArray *arr = [XMLController arrayWithKey:@"email" fromDic:dic]; return [arr count]; } return 0; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if(cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; }
if(indexPath.section == 0) { if(indexPath.row == 0) cell.textLabel.text = [XMLController stringWithKey:@"name" fromDic:contact]; else if(indexPath.row == 1) cell.textLabel.text = [XMLController stringWithKey:@"address" fromDic:contact]; } else if(indexPath.section == 1) { NSDictionary *dic = [contact objectForKey:@"phones"]; NSArray *arr = [XMLController arrayWithKey:@"phone" fromDic:dic]; dic = [arr objectAtIndex:indexPath.row]; cell.textLabel.text = [XMLController stringWithKey:@"phone" fromDic:dic]; } else if(indexPath.section == 2) { NSDictionary *dic = [contact objectForKey:@"emails"]; NSArray *arr = [XMLController arrayWithKey:@"email" fromDic:dic]; dic = [arr objectAtIndex:indexPath.row]; cell.textLabel.text = [XMLController stringWithKey:@"email" fromDic:dic]; }
return cell; } |
위 코드들의 소스를 첨부파일로 올려놓겠습니다.