はじめに:Swift1.1 → Swift1.2 → Swift2.xで変更があったNSXMLParser
現在はAPIから情報を取得してUITableViewに表示するような処理をする場合、レスポンスはJSONで受け取る場合が多いかと思うのですが、今回はSwift2.0でXMLの解析&読み込みをする場合の備忘録として記載しておきます。
(公開されているAPIはほとんどどちらの形式もサポートしている場合が多い)
注意点:parserメソッドのattributes attributeDict:がバージョンによって異なっている
parserメソッドについては特に大きな変更点があったのは下記の部分になります。
またそれによって処理や他のparserメソッドについてもoptionalの位置も微妙に違ってくるところには注意して下さい。
Swift1.1)
parser(parser: NSXMLParser!, didStartElement elementName: String!, namespaceURI: String!, qualifiedName qName: String!, attributes attributeDict: NSDictionary!)
↑attributeDictはNSDictonary!
Swift1.2)
parser(parser: NSXMLParser!, didStartElement elementName: String!, namespaceURI: String!, qualifiedName qName: String!, attributes attributeDict: [NSObject : AnyObject]!)
↑attributeDictは[NSObject : AnyObject]!
Swift2.x)
parser(parser: NSXMLParser!, didStartElement elementName: String!, namespaceURI: String!, qualifiedName qName: String!, attributes attributeDict: [String : String])
↑attributeDictは[String : String]
こんな感じでそれぞれバージョンが異なると当然処理も変わってくるため、Swift2.0でXMLを解析する際のポイントをざっくりとまとめようと思います。
コード:実際の処理でポイントになる部分はここ
■ 使用API:
このサンプルでは「お菓子の虜」APIを利用しています。
http://www.sysbird.jp/toriko/webapi/
- 実際のこのAPIのレスポンスに関しては「お菓子の虜」のページを参照してください
- 今回は上記APIを使用してランダムでデータを30件取得する想定で書いています
- UITableViewに表示するのは以下の5つ
- お菓子名
- お菓子のサムネイル
- お菓子のメーカー名
- お菓子のカテゴリー
- お菓子の値段
※あまり綺麗な処理ではありませんが参考になれば幸いです。
1. Delegateや変数の用意
//NSXMLParserDelegateを記載
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSXMLParserDelegate
2. メンバ変数や解析データを格納する変数を用意
//XMLのフィード取得用URL
let feedUrl : NSURL = NSURL(string:"http://www.sysbird.jp/webapi/?apikey=guest&max=30&order=r")!
//XMLの現在要素名を入れる変数
var currentElementName : String!
//取得する要素名(とりはじめの要素)
let itemElementName : String = "item"
//取得する要素名の決定(item要素の下にあるもの)
let nameElementName : String = "name"
let makerElementName : String = "maker"
let priceElementName : String = "price"
let typeElementName : String = "type"
let urlElementName : String = "url"
let imageElementName : String = "image"
//各エレメント用の変数
var posts = NSMutableArray()
var elements = NSMutableDictionary()
var element = NSString()
var name = NSMutableString()
var maker = NSMutableString()
var price = NSMutableString()
var type = NSMutableString()
var url = NSMutableString()
var image = NSMutableString()
3. viewDidLoad内の処理
override func viewDidLoad() {
super.viewDidLoad()
//解析データ格納用
posts = []
//UITableViewに関する処理を記述
...(省略)...
//NSXMLParserクラスのインスタンスを準備
let parser : NSXMLParser = NSXMLParser(contentsOfURL: feedUrl)!
//XMLパーサのデリゲート
parser.delegate = self
//XMLパースの実行
parser.parse()
}
4. XMLパース処理実行開始時に行う処理
//今回は特にないので空のまま
func parserDidStartDocument(parser: NSXMLParser) {
}
5. XMLパース処理実行中に行う処理(NSXMLParser)
item要素を見つける
※XMLの要素を上から順番に調べていくイメージで処理が行われる。
func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
self.element = elementName
//要素名が「item」の要素が見つかった場合には上で設定したメンバ変数を初期化
if (elementName as NSString).isEqualToString(self.itemElementName){
self.elements = [:]
self.name = ""
self.maker = ""
self.price = ""
self.type = ""
self.url = ""
self.image = ""
}
}
6. XMLパース処理実行中に行う処理(NSXMLParser)
item要素内からさらにname・url・price・maker・url・image要素を見つけてitem要素を見つけた際に用意した入れ物に入れてあげる。
func parser(parser: NSXMLParser, foundCharacters string: String){
//もっとエレガントに書かねば...
if self.element.isEqualToString(self.nameElementName) {
self.name.appendString(
strip(string)
)
}
if self.element.isEqualToString(self.makerElementName) {
self.maker.appendString(
strip(string)
)
}
if self.element.isEqualToString(self.priceElementName) {
self.price.appendString(
strip(string)
)
}
if self.element.isEqualToString(self.typeElementName) {
self.type.appendString(
strip(string)
)
}
if self.element.isEqualToString(self.urlElementName) {
self.url.appendString(
strip(string)
)
}
if self.element.isEqualToString(self.imageElementName) {
self.image.appendString(
strip(string)
)
}
}
このままだとXMLの要素を取得する際に余分な改行や半角スペースが含まれてしまうので、改行と半角スペースの除去をするメソッドも追加しておくことにします。(正規表現を使うべきだな...)
//改行と半角スペースの除去
func strip(str: String) -> String {
var strBr: String
var strSp: String
//改行除去
strBr = str.stringByReplacingOccurrencesOfString("\n", withString: "", options: [], range: nil)
//半角スペース除去
strSp = strBr.stringByReplacingOccurrencesOfString(" ", withString: "", options: [], range: nil)
return strSp
}
7. XMLパース処理実行中に行う処理(NSXMLParser)
タグの最後を検出した際に取り出したい要素に合致していた場合は、解析データ格納用へ入れる
func parser(parser: NSXMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
if (elementName as NSString).isEqualToString(self.itemElementName) {
//各メンバ変数がnilでなければメンバ変数elementsへ「キー」と「値」のペアを格納
if !self.name.isEqual(nil) {
self.elements.setObject(self.name, forKey: self.nameElementName)
}
if !self.maker.isEqual(nil) {
self.elements.setObject(self.maker, forKey: self.makerElementName)
}
if !self.price.isEqual(nil) {
self.elements.setObject(self.price, forKey: self.priceElementName)
}
if !self.type.isEqual(nil) {
self.elements.setObject(self.type, forKey: self.typeElementName)
}
if !self.url.isEqual(nil) {
self.elements.setObject(self.url, forKey: self.urlElementName)
}
if !self.image.isEqual(nil) {
self.elements.setObject(self.image, forKey: self.imageElementName)
}
self.posts.addObject(self.elements)
}
}
おわりに. Swiftのバージョンアップの際はきちっと公式ドキュメントで確認しておこう
ソース的にはあまり綺麗とは言えませんが、XMLの解析処理の部分を作成する際の参考に少しでもお役に立つことができれば嬉しいです。
おまけというかQuestion. 画像を取得する処理はこれでいいのだろうか...
//画像 ※image要素のデータを取得した後にnilの可能性をチェック
let q_global: dispatch_queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
let q_main: dispatch_queue_t = dispatch_get_main_queue();
let imageParameter: String! = (posts.objectAtIndex(indexPath.row).valueForKey("image")!.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
//URLがあれば画像取得処理を実行
if imageParameter != "" {
//サムネイルのURLをもとに画像データ(NSData型)を作成
let imageURL = NSURL(string: imageParameter)
//非同期でURLデータを取得
dispatch_async(q_global,{
//サムネイルのURLをもとに画像データ(NSData型)を作成
var error: NSError?
var imageData: NSData?
//データが取得できれば正常処理
do {
imageData = try NSData(contentsOfURL: imageURL!, options: [])
//Errorが返された場合はimageDataにnilを入れる
} catch let error1 as NSError {
error = error1
imageData = nil
//それ以外の例外
} catch {
fatalError()
}
if error != nil {
//nilの時はデフォルトイメージを表示してあげる
let image: UIImage = UIImage(named: "no_image.gif")!
cell!.okashiImage?.image = image
}
//更新はメインスレッドで行う
dispatch_async(q_main,{
//イメージデータがnilでなければサムネイル画像を表示
if((imageData) != nil){
//xibのサムネイルエリアに表示する
let image: UIImage = UIImage(data: imageData!)!
cell!.okashiImage?.image = image
cell!.layoutSubviews()
}
})
})
} else {
//nilの時はデフォルトイメージを表示してあげる
let image: UIImage = UIImage(named: "no_image.gif")!
cell!.okashiImage?.image = image
}
実際に動いているサンプル:
https://github.com/fumiyasac/XMLFeedSample
※(最終バージョン)2015/11/25
※リファクタリングも近日行う予定です。また処理に関するご指摘もお気軽にどうぞ!