본문 바로가기

iOS Programming

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;

}





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


testXMLController.zip