書いて覚えるSwift入門

第5回 遺産の継承(その2)

この記事を読むのに必要な時間:およそ 4 分

前回に引き続き,SwiftからCやObjective-Cの遺産を今回も活用します。前回はおもにC――厳密にはlibc――の機能をSwiftから使うにはどうしたらよいかを見ていきましたが,今回はObjective-C,つまりフレームワークをどう活用していくかを見ていくことにしましょう。

import(Cocoa | UIKit)

Objective-CではCにおけるlibcに相当するのが,OS XではCocoaiOSではUIKitです。ただしlibcよりできることははるかに多彩です。たとえば,あるURLにアクセスしてそのコンテンツを表示するというのは,Webすらなかった時代に作られたlibcでは簡単には書けませんが,CocoaUIKitであればわずかこれだけですリスト1)。

リスト1 CocoaやUIKitのサンプル

import Cocoa // OS X の場合。iOS ならUIKit
let url = "http://example.com/"
var enc = NSUTF8StringEncoding
var err:NSError?
if let content = NSString(
  contentsOfURL: NSURL(string:url)!,
  usedEncoding:&enc,
  error:&err
) {
    println(content)
} else {
    println(err)
}

なんと,NSString()という文字列を初期化するAPIに適切なパラメータを渡すだけで,ソケットを初期化し,HTTPプロトコルでサーバにアクセスし,その内容をGETしてくれるわけです。あまりに楽なので,スクリプト言語でプログラミングしているような感覚です図1)。

図1 リスト1の実行結果

図1 リスト1の実行結果

この場合URLのコンテンツはテキストですが,JSONをパースする機能すら基本装備していますリスト2)。

リスト2 JSONのパース機能を備えている

import Cocoa
let url = "http://api.dan.co.jp/asin/4534045220.json"
var enc = NSUTF8StringEncoding
var err:NSError?
if let content = NSString(
  contentsOfURL: NSURL(string:url)!,
  usedEncoding:&enc,
  error:&err
  ) {
    if let json = NSJSONSerialization.JSONObjectWithData(
        content.dataUsingEncoding(enc)!,
        options: nil, error: &err) {
        println(json)
    } else {
        println(err)
    }
} else {
    println(err)
}

しかしパースしたJSONから特定のアイテムを抜き取りたいとなると,かなり面倒なことになりますリスト3)。

リスト3 JSONから特定のアイテムを抜き出す

import Cocoa
let url = "http://api.dan.co.jp/asin/4534045220.json"
var enc = NSUTF8StringEncoding
var err:NSError?
if let content = NSString(contentsOfURL: NSURL(string:url)!, usedEncoding:&enc, error:&err) {
    if let json:AnyObject = NSJSONSerialization.JSONObjectWithData(
        content.dataUsingEncoding(enc)!,
        options: nil, error: &err) {
            if let item = json["ItemAttributes"] as? NSDictionary {
                if let author = item["Author"] as? NSString {
                    println(author)
                }
            }
    } else {
        println(err)
    }
} else {
    println(err)
}

JSONをサポートする多くの言語でjson["ItemAttributes"]["Author"]と一度に書けるところを,まずlet item = json["ItemAttributes"] as? NSDictionaryitemを取り出し,さらにlet author =item["Author"] as? NSStringNSStringを取り出しという具合に,型が静的であるというSwiftの特徴がアダとなってしまっています。どうにかしてjson["Item Attributes"]["Author"]と書く方法はないでしょうか? さらに可能ならJavaScriptのようにjson.ItemAttributes.Authorと書けないのでしょうか?

ラッパーのススメ

その試みがSwiftyJSONであり,拙作のSwift-JSONです。たとえばSwift-JSONならリスト3のコードは

let author = JSON(url:"http://api.dan.co.jp/asin/4534045220.json")["ItemAttributes"]["Author"].asString

と1行で済んでしまいます。さらにスキーマをclassとして実装すれば,リスト4のようにすら書けます。

リスト4 ラッパーの使用例

class ASIN : JSON {
    override init(_ obj:AnyObject){ super.init(obj) }
    override init(_ json:JSON) { super.init(json) }
    var ItemAttributes: ASIN { return ASIN(self["ItemAttributes"]) }
    var Author: String { return self["Author"].asString! }
}

let author = ASIN(url:"http://api.dan.co.jp/asin/4534045220.json").ItemAttributes.Author

SwiftyJSONやSwift-JSONはこれをどのように実現しているのでしょうか? ソースコード全体を読んでいただければ一目瞭然なのですが,SwifyJSONは1,163行,Swift-JSONは432行で紙幅にとても収まりません(2015年3月当時)。ここではSwift-JSONのキモだけ解説します。

Swift-JSONのインスタンス変数は,たった1つです。

public class JSON {
    private let _value:AnyObject
    // ....
}

これに対し,subscriptは2種類定義されていますリスト5,リスト6)。

リスト5 subscript(idx:Int)

    public subscript(idx:Int) -> JSON {
        switch _value {
        case let err as NSError:
            return self
        case let ary as NSArray:
            if 0 <= idx && idx < ary.count {
                return JSON(ary[idx])
            }
            return JSON(NSError(
                domain:"JSONErrorDomain", code:404, userInfo:[
                    NSLocalizedDescriptionKey:
                    "[\(idx)] is out of range"
                ]))
        default:
            return JSON(NSError(
                domain:"JSONErrorDomain", code:500, userInfo:[
                    NSLocalizedDescriptionKey: "not an array"
                ]))
            }
    }

リスト6 subscript(key:String)

    public subscript(key:String)->JSON {
        switch _value {
        case let err as NSError:
            return self
        case let dic as NSDictionary:
            if let val:AnyObject = dic[key] { return JSON(val) }
            return JSON(NSError(
                domain:"JSONErrorDomain", code:404, userInfo:[
                    NSLocalizedDescriptionKey:
                    "[\"\(key)\"] not found"
                ]))
        default:
            return JSON(NSError(
                domain:"JSONErrorDomain", code:500, userInfo:[
                    NSLocalizedDescriptionKey: "not an object"
                ]))
            }
    }

つまり,json[0]のように添え字がIntであればインスタンス変数をNSArrayとみなし,json["name"]のように添え字がStringであればNSDictionaryとみなして,その要素から新たなJSONオブジェクトを生成しているわけです。そして要素が存在しない場合は,NSErrorからJSONオブジェクトを生成し,インスタンス変数がNSErrorの場合はそのまま自分自身を返すことで,HaskellのEitherが一度NothingになればずっとNothingであるように,最初に発生したエラーが引き継がれるというわけです。

このようなラッパーは同等の機能をフルスクラッチでSwiftで書くよりずっと簡単に書けますし,書くことによってSwiftとObjective-Cの連携がどのようになされているかを体得することもできます。読者の皆さんも,これぞというものがあったらぜひ書いて,GitHubなどで公開してみてください。

著者プロフィール

小飼弾(こがいだん)

1969年生まれ,東京都出身。元ライブドア取締役の肩書きよりも,最近はPokemon GOのガチトレーナーのほうが有名になりつつある……かもしれない永遠のエンジニアオヤジ。

活躍の場はIT業界だけでなく,サブカルからアカデミックまで多方面にわたり,ネットからの情報発信は気の向くまま毎日毎秒! https://twitter.com/dankogai,ニコニコチャンネルは,http://ch.nicovideo.jp/dankogai,blogはhttp://blog.livedoor.jp/dankogai/

当社刊行書籍は『小飼弾のアルファギークに逢ってきた』『小飼弾のコードなエッセイ』など。他にも著書多数。

コメント

コメントの記入