読者です 読者をやめる 読者になる 読者になる

mitsu's techlog

通信ネットワークの研究してる大学院生です。備忘録も兼ねて技術系の話を適当に

SwiftyJson コードリーディングしてみた

Swift

SwiftyJsonをコードリーディングしてみました。

僕はobjective-cは1年近く触っていますが、swiftはまだ全然勉強していないという人です。 複雑な処理がないので初心者でも読みやすく、subscriptやSequenceTypeといった基本的なことがわかるのでオススメです。

SwiftyJsonとは

SwiftyJsonとはswiftjsonを使うときにはほぼデファクトとなっているライブラリです。 以下のような感じで簡単にjsonを使えるので便利です。

let json: JSON = JSON(data: dataFromNetworking)
let user: <String, JSON> = json["user"]. dictionaryValue
let name: String = json["user"]["name"].string

さて、コードリーディングをして気になった部分をメモしていきます。

変数の型のチェック方法

型のチェックはobject:AnyObjectのsetterの中で以下のようにas を利用して書かれていました。

あと、swiftのswitch文ではbreakが必要無いんですね。逆に続けて処理をしたい時はfallthroughと書くらしいです。 switch文の中では似たような方法でlet n where n < 10のように範囲指定したりもできるそうです。タプルも使えるみたいなので、swiftのswitch文の柔軟性は凄いですね。

switch newValue {
case let number as NSNumber:
    if number.isBool {
        _type = .Bool
    } else {
        _type = .Number
    }
case let string as NSString:
    _type = .String
case let null as NSNull:
    _type = .Null
case let array as [AnyObject]:
    _type = .Array
case let dictionary as [String : AnyObject]:
    _type = .Dictionary
default:
    // error handling
}

number.isBoolは以下のようにextensionを書いて実装していました。 swiftではtrue/falseを1/0で表現しているためBoolのtrueかNumberの1か判断できないのですが、 String.fromCString(trueNumber.objCType)を利用することで判断しています。

NSNumberはプリミティブ型のラッパークラスなのでbool, int, long, floatなど様々な値を代入することができ、objCTypeプロパティはその中のどれが代入されているのかを示すものです(多分)。playgroundで確認したところ、Boolならば"c"が返ってきました。

private let trueNumber = NSNumber(bool: true)
private let falseNumber = NSNumber(bool: false)
private let trueObjCType = String.fromCString(trueNumber.objCType)
private let falseObjCType = String.fromCString(falseNumber.objCType)

extension NSNumber: Swift.Comparable {
    var isBool:Bool {
        get {
            let objCType = String.fromCString(self.objCType)
            if (self.compare(trueNumber) == NSComparisonResult.OrderedSame &&  objCType == trueObjCType) ||  (self.compare(falseNumber) == NSComparisonResult.OrderedSame && objCType == falseObjCType){
                return true
            } else {
                return false
            }
        }
    }
}

subscript

subscriptとは[ ]で要素にアクセスするやつです。 SwiftyJsonでは以下のようにpathをarrayとして渡してアクセスすることができます。

let path = [9,"list","person","name"] 
let name = json[path]

パスのArrayの中は、ArrayのときにInt、DictionaryのときにStringを受け取ることができるので、IntとStringにSubscriptTypeプロトコルを追加し、[SubscriptType]を引数に受け取るように実装してありました。

Arrayの中に複数の型が入っているときは[AnyObject]とするのではなく、このように実装すると安全で良いですね。

public protocol SubscriptType {}
extension Int: SubscriptType {}
extension String: SubscriptType {}

extension JSON {
    public subscript(path: [SubscriptType]) -> JSON {
        get {
            if path.count == 0 {
                return JSON.nullJSON
            }
         
            var next = self
            for sub in path {
                next = next[sub:sub]
            }
            return next
        }
        set {
           // setter
        }
    }
}

subscriptの中ではエラーチェックの後、配列で渡されたパスをfor文を用いて一つづつ処理しています。 一つづつの処理はInt(Array)とString(Dictionary)のときで処理が変わるので振り分けるだけのメソッドを用意してあります。

その中の分岐でsub is Stringという構文が使われていました。 switch文の中では case let string as String、if文の中では sub is Stringとすることで型チェックができるみたいです。

private subscript(#sub: SubscriptType) -> JSON {
        get {
            if sub is String {
                return self[key:sub as! String]
            } else {
                return self[index:sub as! Int]
            }
        }
        set {
            // setter
        }
}

SequenceType

SwiftyJsonではjsonがArray/Dictionaryのときにfor..inを使ったループ処理ができます。

for (key: String, subJson: JSON) in json {
   //Do something you want
}

これを実現するためにはSequenceTypeプロトコルを実装する必要があります。 SequenceTypeプロトコルはgenerateメソッドを要求しています。

generateメソッドはGeneratorTypeプロトコルを実装しているGeneratorを返す必要があるのですが、今回はArrayとDictionaryを利用するのでGeneratorTypeについては気にする必要はありません。 もし自作クラスにSequenceTypeを適用するときにはGeneratorTypeも実装する必要があります。

SwiftyJsonではgenerateメソッドは以下のようになっています。 ArrayとDictionaryで処理を分けているだけですね。

GeneratorはGeneratorOfでClosureを使って実装しています。

public func generate() -> GeneratorOf <(String, JSON)> {
    switch self.type {
    case .Array:
        let array_ = object as! [AnyObject]
        var generate_ = array_.generate()
        var index_: Int = 0
        return GeneratorOf<(String, JSON)> {
            if let element_: AnyObject = generate_.next() {
                return ("\(index_++)", JSON(element_))
            } else {
                return nil
            }
        }
    case .Dictionary:
        let dictionary_ = object as! [String : AnyObject]
        var generate_ = dictionary_.generate()
        return GeneratorOf<(String, JSON)> {
            if let (key_: String, value_: AnyObject) = generate_.next() {
                return (key_, JSON(value_))
            } else {
                return nil
            }
        }
    default:
        return GeneratorOf<(String, JSON)> {
            return nil
        }
    }
}

Literal convertibles

Literal convertiblesとは以下のように異なる型でも代入できるようにすることです。 以下の例ではjsonJSON型として宣言されているのでStringは代入できないはずですが、JSONがStringLiteralConvertibleプロトコルを実装しているのでStringを代入することができます。

let json: JSON = "I'm a json"

StringLiteralConvertibleの他にはIntegerLiteralConvertibleやBooleanLiteralConvertibleなどもあります。 これらのプロトコルを実装することで異なる型の代入を可能にしています。

実装が必要なメソッドプロトコルによって異なりますが、基本的には内部で準備をして、initを呼ぶだけです。 SwiftyJsonではIntegerのときはinitを呼ぶだけですが、Dictionaryのときは引数をdictionaryに格納してからinitを読んでいることがわかります。 これは引数をArrayで受け取るため、Dictionaryに変換しています。

extension JSON: Swift.IntegerLiteralConvertible {
    public init(integerLiteral value: IntegerLiteralType) {
        self.init(value)
    }
}

extension JSON: Swift.DictionaryLiteralConvertible {
    public init(dictionaryLiteral elements: (String, AnyObject)...) {
        var dictionary_ = [String : AnyObject]()
        for (key_, value) in elements {
            dictionary_[key_] = value
        }
        self.init(dictionary_)
    }
}