Cocoa/CocoaTouchからRemember The Milk APIを使ってみる(その5:レスポンスの解析再挑戦)

Cocoa/CocoaTouchからRemember The Milk APIを使ってみる(その4:レスポンスの解析) の方法には重大な問題があった。
XMLの要素名がsで終わっていれば内容が配列と判断するので、sだけれども実際には配列でない場合、sでないのに配列な場合は正しい結果が得られない。この問題を修正するためにはどうすればいいか、小人さん会議をした結果。

親要素の中に複数存在する場合はNSArrayにする。

これならどんな場合にも対応出来る。ただし、通常複数ある要素がたまたま一つしかない場合、アクセス方法が変わってしまう。

常にNSArrayにする。

NSDictionaryのツリーではなく、NSArrayのツリーにする。ただし、特定のキーにアクセスする場合NSArrayの中身を順番に引っ張り出しては要素名を比較するという面倒なことになる。

常にNSDictionary、NSArrayを併用する。

今回採った方法。要素名をキーにして親要素が持っているNSDictionaryにセットしていくと同時に、親要素が持っているNSArrayにも追加していく。当然同じオブジェクトが二カ所に保持されていることになるが、オブジェクトの数自体が増えるわけではない。

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName
	attributes:(NSDictionary *)attributeDict {
	
	NSMutableDictionary *parentElement = [elementPath lastObject];
	NSMutableDictionary *currentElement = [NSMutableDictionary dictionary];
	
	// am i root?
	if (!parentElement) {
		self.response = currentElement;
		self.elementPath = [NSMutableArray array];
	} else {
		
		// put me into _contents array
		NSMutableArray *parentContents = [parentElement objectForKey:@"_contents"];
		if (!parentContents) {
			parentContents = [NSMutableArray array];
			[parentElement setObject:parentContents forKey:@"_contents"];
		}
		[parentContents addObject:currentElement];
		
		// set me for key
		[parentElement setObject:currentElement forKey:elementName];
		
	}
	
	[currentElement setObject:elementName forKey:@"_elementName"];
	
	if (attributeDict)
		[currentElement addEntriesFromDictionary:attributeDict];
	
	[elementPath addObject:currentElement];
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
	NSMutableDictionary *currentElement = [elementPath lastObject];
	NSMutableString *currentContent = [currentElement objectForKey:@"_string"];
	if (!currentContent) {
		currentContent = [NSMutableString string];
		[currentElement setObject:currentContent forKey:@"_string"];
	}
	[currentContent appendString:string];
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName 
  namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
	
	[elementPath removeLastObject];
}

ついでにattributesを別のNSDictionaryに入れずにそのままNSDictionaryに突っ込んだり、要素の内容が文字列の場合は_stringキーに変更(_contentsと紛らわしいので)などしている。

こうして出来たレスポンス(NSDictionary)からは、例えばこうやって情報を取り出す。
http://www.rememberthemilk.com/services/api/methods/rtm.auth.getToken.rtm

<auth>
  <token>6410bde19b6dfb474fec71f186bc715831ea6842</token>
  <perms>delete</perms>
  <user id="987654321" username="bob" fullname="Bob T. Monkey" />
</auth>

XMLでこういうレスポンスだったら、

[response valueForKeyPath:@"auth.token._string"]

これでauthの中のtokenの中の文字列を得る。

[response valueForKeyPath:@"auth.user.username"]

これだったらbobが返ってくるはず。

http://www.rememberthemilk.com/services/api/methods/rtm.lists.getList.rtm
こういうレスポンスだったら、

[response valueForKeyPath:@"lists._contents"]

でNSArrayに入ったNSDictionary達が得られる。KeyPathの本領を発揮すれば、

[response valueForKeyPath:@"lists._contents.name"]

これでNSArrayに入ったリストの名前達が直接得られる。ええ、まあvalueForKeyPath:を(簡単に)使いたいからこそNSDictionaryに入れたんだけど。