iOS Programming

XML 데이터를 NSArray와 NSDictionary로 구조화하자.

Joon~~~ 2012. 8. 27. 10:22

서버 연동 앱을 개발하다 보면 서버로 부터 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;

}





위 코드들의 소스를 첨부파일로 올려놓겠습니다.


testXMLController.zip