Programming

(処理解説編)XMLFeedSampleを作成しました。

 Author:fumiyasac

スクリーンショット 2015-03-04 23.42.10

3/7にSwiftビギナーズ倶楽部でXMLでデータを読みこんでUITableViewで一覧表示→UIWebViewで詳細表示するサンプルにつきまして解説をする予定です。
その際に使うサンプルに関して、事前に押さえておきたいポイントを解説できればと思います。前回は事前準備編を書きましたが、今回は実際のXMLの解析処理とUITableViewへの表示までを解説したいと思います。

1. 準備

◆準備編はこちら
(事前予習編)XMLFeedSampleを作成しました。

※今回はUITableView自体の解説は省略していますが、UITableViewも実務ではよく使うものになりますので、あらかじめ調べておくと良いかと思います。

◆解析対象のXML

http://www.sysbird.jp/webapi/?apikey=guest&max=30&order=r

◆表示するXMLのひとかたまり

  1. <item>
  2.     <id>1149</id>
  3.     <name>おさつどきっ(ゆず香り塩)</name>
  4.     <kana>おさつどきっゆずかおりしお</kana>
  5.     <maker>UHA味覚糖</maker>
  6.     <price>138</price>
  7.     <type>snack</type>
  8.     <regist>2008年9月23日</regist>
  9.     <url>http://www.sysbird.jp/toriko/2008/09/23/%E3%81%8A%E3%81%95%E3%81%A4%E3%81%A9%E3%81%8D%E3%81%A3%E3%82%86%E3%81%9A%E9%A6%99%E3%82%8A%E5%A1%A9/</url>
  10.     <tags>
  11.         <tag>おさつどきっ</tag>
  12.         <tag>さつまいも</tag>
  13.         <tag>ゆず</tag>
  14.         </tags>
  15.     <image>http://www.sysbird.jp/toriko/wp-content/blogs.dir/2/files/1149.gif</image>
  16.     <comment><p>さつまいもにユズなんて、おしゃれな和菓子みたいな組み合わせだ。紫のパッケージも、どことなく大人っぽいではないか。開封するとユズのいい匂いが、プーンと漂った。甘いなかに塩味がきいており、味はプレーンに近い。日本茶と一緒に食べると、ほんとに落ち着く。この秋は「おさつどきっ」が本気だ。</p></comment>
  17. </item>

ここで今回の表示用に必要なタグ名をあらかじめ決めておきます。

  1. 現在の解析位置を格納する変数
  2. 解析対象のXMLの要素名の定数

を下記のように定義をしておきます。

  1. //解析中の要素名を入れておく変数
  2. var currentElementName : String!
  3. //取得する要素名の決定(とりはじめの要素)
  4. let itemElementName:String = "item"
  5. //取得する要素名の決定(item要素の下にあるもの)
  6. let nameElementName:String = "name"
  7. let makerElementName:String = "maker"
  8. let priceElementName:String = "price"
  9. let typeElementName:String = "type"
  10. let urlElementName:String = "url"
  11. let imageElementName:String = "image"

同じようなメソッドがたくさんあるので、ぱっと見「何をしているのかよくわからない!」と思いますので、ここでは簡単ではありますが処理の流れや使用しているメソッドに関しての解説を行っていきたいと思います。

2. XMLパース処理実行開始時に行う処理

◆ViewController.swift

  1. func parserDidStartDocument(parser: NSXMLParser!) {
  2.     ...(処理を記載)
  3. }

今回のサンプルでは特に何もすることはなかったので、一応用意だけはしておくけど処理は何も記載をしていないような形です。

3. タグの最初を検出

◆ViewController.swift

  1. func parser(parser: NSXMLParser!, didStartElement elementName: String!, namespaceURI: String!, qualifiedName qName: String!, attributes attributeDict: NSDictionary!) {
  2.     ...(処理を記載)
  3. }

この処理内では上から順番にXMLの要素を一つ一つ解析していきます。

◆Case1.
一番最初の要素名(この場合は"item")と一致したときは、item内の各要素を入れるための入れ物を1個分用意してあげる

◆Case2.
一番最初の要素名(この場合は"item")と一致しないときは、今現在の要素名を変数currentElementNameに入れてあげる

という処理を記載します。大まかな処理としては上から順番に要素名を見ていってXMLの解析を行っているような形になります。

4. 実際のパース処理

◆ViewController.swift

  1. func parser(parser: NSXMLParser!, foundCharacters string: String!){
  2.     //itemsの中に空っぽの入れ物が準備できている場合の処理
  3.     if items.count > 0 {
  4.         //空っぽの入れ物のなかにそれぞれの要素の文字列を入れる
  5.         var lastItem = items[items.count-1]
  6.         if currentElementName? == nameElementName {
  7.             //name要素を入れる
  8.             //一番最初に取ってくるものについてはこの書き方(もしname要素内にすでに値があったら追記する)
  9.             var tmpString : String? = lastItem.name
  10.             lastItem.name = (tmpString != nil) ? tmpString! + string : string
  11.         } else if currentElementName? == urlElementName {
  12.             //url要素を入れる
  13.             lastItem.url = string
  14.         } else if currentElementName? == makerElementName {
  15.             //maker要素を入れる
  16.             lastItem.maker = string
  17.         } else if currentElementName? == typeElementName {
  18.             //type要素を入れる
  19.             lastItem.type = string
  20.         } else if currentElementName? == priceElementName {
  21.             //price要素を入れる
  22.             lastItem.price = string
  23.         } else if currentElementName? == imageElementName {
  24.             //image要素を入れる
  25.             lastItem.thumb = string
  26.         }
  27.     }
  28. }

この部分では取得したそれぞれの要素を、「3. タグの最初」のところで作った入れ物の中に順次入れていく処理を行います。変数currentElementNameの値と抜き出す対象の定数が一致した際に入れ物に入れていくようなイメージです。

※若干処理が冗長なんですけど、どの要素名のデータを入れるのかを押さえておくとよいと思います。

5. タグの最後を検出

◆ViewController.swift

  1. func parser(parser: NSXMLParser!, didEndElement elementName: String!, namespaceURI: String!, qualifiedName qName: String!) {
  2.     ...(処理を記載)
  3. }

こちらはタグの最後に来た際の処理を記載します。
タグの最後に来たよという意味をこめて、変数currentElementNameにnil入れてあげるだけです。

6. XMLパース処理実行終了時に行う処理

◆ViewController.swift

  1. func parserDidEndDocument(parser: NSXMLParser!){
  2.     ...(処理を記載)
  3. }

こちらはXMLのパース処理が終了した際に行う処理を記載します。
今回はテーブルビューにデータを反映させたいので、テーブルビューのリロード処理を記載しています。
※正しくデータが取れているかの確認もこの部分にprintlnを仕込んで確認するとよいかと思います。

7. XMLパースをしたものをUITableViewに表示する

◆ViewController.swift

  1. //Xibファイルを元にデータを作成する
  2. var cell = tableView.dequeueReusableCellWithIdentifier("apiDataCell") as ApiDataTableViewCell;
  3. var item = items[indexPath.row]
  4. //Xibのプロパティの中にそれぞれの要素名を入れる
  5. //cell.okashiCategory?.text = item.maker as Stringと書くのもOK
  6. //お菓子名
  7. cell.okashiName?.text = "\(item.name)"
  8. //メーカー名
  9. cell.okashiMaker?.text = "\(item.maker)"
  10. //カテゴリー ※nilの可能性があるものはあらかじめチェック
  11. if(item.type != nil){
  12.     //取得した文字列に応じて表記の変更
  13.     if(String(item.type) == "snack"){
  14.         cell.okashiCategory?.text = "スナック"
  15.     }else if(String(item.type) == "chocolate"){
  16.         cell.okashiCategory?.text = "チョコレート"
  17.     }else if(String(item.type) == "cookie"){
  18.         cell.okashiCategory?.text = "クッキー・洋菓子"
  19.     }else if(String(item.type) == "candy"){
  20.         cell.okashiCategory?.text = "飴・ガム"
  21.     }else if(String(item.type) == "senbei"){
  22.         cell.okashiCategory?.text = "せんべい・和風"
  23.     }
  24. }else{
  25.     cell.okashiCategory?.text = "-"
  26. }
  27. //価格 ※nilの可能性があるものはあらかじめチェック
  28. if(item.price != nil){
  29.     cell.okashiPrice?.text = "\(item.price)"
  30. }else{
  31.     cell.okashiPrice?.text = "-"
  32. }
  33. //画像 ※image要素のデータを取得した後にnilの可能性をチェック
  34. var q_global: dispatch_queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  35. var q_main: dispatch_queue_t = dispatch_get_main_queue();
  36. //サムネイルのURLをもとに画像データ(NSData型)を作成
  37. var imageURL = NSURL(string: item.thumb as String)?
  38. //URLがあれば画像取得処理を実行
  39. if(imageURL != nil){
  40.     //非同期でURLデータを取得
  41.     dispatch_async(q_global,{
  42.         //サムネイルのURLをもとに画像データ(NSData型)を作成
  43.         var imageData = NSData(contentsOfURL: imageURL!)
  44.         //更新はメインスレッドで行う
  45.         dispatch_async(q_main,{
  46.             //イメージデータがnilでなければサムネイル画像を表示
  47.             if((imageData) != nil){
  48.                 //xibのサムネイルエリアに表示する
  49.                 var image: UIImage = UIImage(data: imageData!)!
  50.                 cell.okashiImage?.image = image
  51.                 cell.layoutSubviews()
  52.             }
  53.         })
  54.     })
  55. }else{
  56.     //nilの時はデフォルトイメージを表示してあげる
  57.     var image: UIImage = UIImage(named: "no_image.gif")!
  58.     cell.okashiImage?.image = image
  59. }

XMLで読み込んだデータをUITableViewに表示する処理になります。
またXMLから取得したデータを表示する場合では「nil」になる(該当データがなくて要素がそもそも存在しない)ケースを考慮しないといけないのでif文が複雑になってます。。
※Optionalのハンドリングでここが実は一番はまった部分です。

残りの処理に関しては、githubに記載したコメントを参照して頂ければ実装はできるかと思います。

8. その他解説を作成するにあたり参考にした資料等

XML関連の記述に関しての参考URL
http://qiita.com/to-takahashi/items/6b757dbda6d1e87e7a73
http://ashishkakkad.com/2014/10/xml-parsing-in-swift-language-ios-8-nsxmlparser/http://m-shige1979.hatenablog.com/entry/2014/10/20/080000

NSXMLParser
公式リファレンス

9. まとめ

今回のサンプルは結構細かい部分の実装もしているサンプルになるので、もしかすると0から始めるとなると、複雑に感じる部分があるかもしれません。

データとの連携処理は最初は難しく感じる部分はありますが(特にOptionalを考慮しなきゃいけないあたりはやっぱハマる)処理の書き方のパターンを1つでも知っておくことで、サーバーサイドとの連携等をする選択肢ができるので、もっとアプリ開発が楽しくなるのではと感じています。

※自分の本業がサーバーサイドのエンジニアをしているからかもしれませんが、アプリとサーバーサイドの両方を知ることで今以上に楽しく開発に取り組むことができているのは間違いないです。