SwiftyJSONのAlamoコンボ

SwiftyJSON · GitHub

Alamofireで楽にネット上のAPIへアクセスできた後は楽に結果を処理したい。APIの戻りはJSONのことが多いですから、JSONが楽に処理できれば様々なことが捗ります。そこで探すと、このSwiftyJSONを見つけました。このGithubのグループはAlamofireと密接にインテグレーションしたものもサブプロジェクトとして用意しています。

class MYCoverCell: UITableViewCell {

    @IBOutlet weak var coverPhoto: UIImageView!
    @IBOutlet weak var userPhoto: UIImageView!
    @IBOutlet weak var nameLabel: UILabel!
    
    func settingCover(uid: String) {
        userPhoto.layer.borderColor = UIColor.whiteColor().CGColor
        userPhoto.layer.borderWidth = 2.0
        
        var req = request(.GET, "https://graph.facebook.com/\(uid)/picture?type=large")
        req.response { (_, _, image, _) in
            dispatch_async(dispatch_get_main_queue()) { () in
                self.userPhoto.image = UIImage(data: image as NSData)
            }
        }
        req = request(.GET, "https://graph.facebook.com/\(uid)?fields=cover")
        req.responseSwiftyJSON { (_, _, json, _) in
            let source = json["cover"]["source"]
            req = request(.GET, source.stringValue)
            req.response { (_, _, image, _) in
                dispatch_async(dispatch_get_main_queue()) { () in
                    self.coverPhoto.image = UIImage(data: image as NSData)
                }
            }
        }
        req = request(.GET, "https://graph.facebook.com/\(uid)")
        req.responseSwiftyJSON { (_, _, json, _)  in
            let locale = json["locale"].stringValue
            req = request(.GET, "https://graph.facebook.com/\(uid)?locale=\(locale)")
            req.responseSwiftyJSON { (_, _, json, _) in
                dispatch_async(dispatch_get_main_queue()) { () in
                    let name = json["name"]
                    self.nameLabel.text = name.stringValue
                }
            }
        }
    }
}

Alamofire-SwiftJSONで書いて実行するとこんな感じ。Facebookから顔写真とカバー写真とローカル表記の名前を取ってきて、UITableViewにセル表示してみます。

f:id:masataka_k:20141024023132p:plain

さて言いたいことはヤマほどあるんだけどなんですか?このFacebookの細切れ呼び出しは。一発でカバー写真のURLとローカル表記名をくださいよ。/UID?fields=coverでURLを取得してからやっとイメージが取れる、/UIDでLocaleを取得してから/UID?locale=LOCALEでやっとローカル表記名が取れるって。。。これらを一発でまとめてくれていれば上記で全体5回APIを叩いてるところが3回にできるのに。

しかしそのストレスを補ってあまりあるのがSwiftyJSONの簡潔な記述です。そしてAlamofire-SwiftyJSONではextensionでAlamofireのRequestを拡張していますので、スムーズにRequest#responseSwiftyJSONを呼び出すことで、クロージャにSwiftyJSONのJSON型が受け取れます。そしてコレをDictionaryやArrayとしてアクセスしてあげれば良いのです。先日はこんなことしてましたからね(こちらはSLRequestなんで違いますけど、やることは全く一緒です)。

// 戻りのJSONからPage IDとアクセストークンを取得する。
let ret = NSJSONSerialization.JSONObjectWithData(responseData,
    options: NSJSONReadingOptions.AllowFragments, error: nil) as NSDictionary
let pageData = (ret.valueForKey("data") as NSArray)[0] as NSDictionary
let pageID = pageData.valueForKey("id") as String
let pageToken = pageData.valueForKey("access_token") as String

SwiftyJSONのコード見るとまたスマートに実現してんだわ。Swiftのsubscriptという配列アクセッサを再定義する仕組みでJSONの構造を直感的に辿ることができます。最終的に値が欲しい時には#stringValueなどの型別プロパティで取り出します。これだけ直感的に省力で書けるのはホント楽。そして楽に書く一連の中でSwift言語での二つの省略を学びました。

  • クロージャだけを受け取るメソッドは、req.response { / code / } というように丸括弧を省略して代わりに中括弧が置かれたかのよう。これは引数リスト最後のクロージャは外に出せるというのと、デフォルト値が設定された引数は省略できるというのと、結果として引数が無いときは空の引数リストを省略できる、というのの組み合わせ。シンプルで素敵
  • クロージャの引数で使わないものはアンダースコアで受けることができる。これはGo言語でそうだったようにコードの可読性あげるために効果あるので素敵。ただしGoは変数宣言して使わないとコンパイルエラーという徹底が前提にありましたが、残念ながらSwiftにはそこまでは無い。

いつものようにサンプルでは例外処理ゼロです。で、この場合の例外処理ですが確実に抑えないとアプリ吹っ飛ぶので絶対必要です。圏外やユーザー操作などの理由でネットワーク接続が無い場合に、Alamofireのresponseメソッド(not Swifty)はきちんと呼べますが第三引数のdataがnilなため、クロージャ内で想定した対応しないとEXC_BAD_ACCESSで吹っ飛ぶ。また、Alamofire-SwiftyJSONではnilではなく空のJSONが例外時にも約束されてるのでその辺はまだ便利ですが、クロージャの第四引数のerrorを常にnilで潰しているので、JSONパース前に例外があったとしても伝播しないです。よって第三引数のjsonの中身を調べながら進める必要があります。

なによりFacebookAPIがエラー発生時にはその旨をJSONにして正常に返してきますので正常系の中で異常対応しないといけません。その辺まとめて先に作っておいたほうがよいですね。それこそextensionで。