あたも技術ブログ

セットジャパンコーポレーションの社員が運営しています。

【Swift】KVCを用いたJSONパーサー

REST-APIやWebSocketで通信するアプリを作っているとどうしても通らないといけないのがJSONのパース。

普通に実装するとこのようになると思います

class Object: NSObject {
    dynamic var str0: String = "a"
    dynamic var str1: String = "b"
    dynamic var str2: String = "c"
    dynamic var num0: Int = 0
    dynamic var num1: Int = 1
    dynamic var num2: Int = 2
    
    init(dictionary: [String : AnyObject]) {
        super.init()
        str0 = dictionary["str0"] as! String
        str1 = dictionary["str1"] as! String
        str2 = dictionary["str2"] as! String
        num0 = dictionary["num0"] as! Int
        num1 = dictionary["num1"] as! Int
        num2 = dictionary["num2"] as! Int
    }
}

しかし、APIが増えるごとにそれぞれのモデルごとにinitを書いているのは正直疲れます。 今回はKVC(Key Value Coding)を用いて、initを書かずにJSON⇔モデルクラスを実現する方法をSwiftで紹介します。

KVCとは?

キー値コーディングとは、オブジェクトのプロパティに間接的にアクセスするための仕組みです。ア クセサメソッドを呼び出してアクセスしたり、インスタンス変数としてアクセスするのではなく、プ ロパティの識別に文字列を使用してアクセスします。 引用元: https://developer.apple.com/jp/documentation/KeyValueCoding.pdf

要は、プロパティに対して.str1のようにアクセサメソッドやインスタンス変数でアクセスするのではなく、 "str1"といった文字列でプロパティに対してアクセスする手法です。 Objective-CではこのKVCを使用してプライベートなAPIを無理やり叩くこともできました。(Swiftではやったことないけど...)

プロパティ名の取得

オブジェクトからJSONマッピングするためにはKeyとなる物が必要なので、 JSONのKeyに対応するプロパティ名をすべて取得します。

class KVCModel: NSObject {

    class func allKeys() -> [String] {
        
        var names: [String] = []
        var count: UInt32 = 0
        self.classForCoder()
        let properties:UnsafeMutablePointer<objc_property_t> = class_copyPropertyList(self.classForCoder(), &count)
        
        for i: UInt32 in 0..<count {
            let property = properties[Int(i)];
            let cname = property_getName(property)
            let name = String.fromCString(cname)
            
            names.append(name!)
            
        }
        free(properties)
        return names
    }
}
self.classForCoder()

では自身のクラスを取得します。

Objective-Cでいうところの

+ (Class)class;

に対応しています。

イニシャライザでマッピングする JSON->モデル

イニシャライザにJSONから取り出したDictionaryを渡すことで自動的にプロパティに値が設定されるようにします。

class KVCModel: NSObject {
    init(dictionary: [String : AnyObject]) {
        super.init()
        if dictionary.count == 0 {
            return
        }
        
        let allPropertyKeys = self.dynamicType.allKeys()        
        for key: String in allPropertyKeys {
            let val = dictionary[key]
            if val != nil {
                self.setValue(val, forKey: key)
            }
        }
    }

}
self.dynamicType

でも自身のクラスを取得していますが、 こちらはObjective-Cでは

- (Class)class;

に対応しています。

Dictionaryで取り出す モデル->JSON

最後に以下のメソッドを追加し、モデルのプロパティをDictionaryとして取り出せるようにします。

    func dictionaryObject() -> [String : AnyObject] {
        let allKeys = self.dynamicType.allKeys()
        return self.dictionaryWithValuesForKeys(allKeys)
    }

動作

以上までで作成したクラスを継承し、モデルクラスを作成しましょう。

class Object: KVCModel {
    dynamic var str0: String = ""
    dynamic var str1: String = ""
    dynamic var str2: String = ""
    dynamic var num0: Int = 0
    dynamic var num1: Int = 0
    dynamic var num2: Int = 0
}

テストコード

let dic = ["str1" : "str", "str2" : "strstr", "num1": 1, "num2" : 2]
var jsonData: NSData = NSData()
var jsonDic: [String : AnyObject] = [:]

// Dictionary => JSON
do {
    jsonData = try NSJSONSerialization.dataWithJSONObject(dic, options: .PrettyPrinted)
} catch {
    // なんかやる
}

// JSON => Dictionary
do {
    jsonDic = try NSJSONSerialization.JSONObjectWithData(jsonData, options: .AllowFragments) as! Dictionary
} catch {
    // なんかやる
}


// Dictionary => Model
let model: Model = Model(dictionary: jsonDic)

print("str1 : \(model.str1)")
print("str2 : \(model.str2)")
print("num1 : \(model.num1)")
print("num2 : \(model.num2)")
print("dic : \(model.dictionaryObject())")

ログ

str1 : str
str2 : strstr
num1 : 1
num2 : 2
dic : ["num2": 2, "str2": strstr, "num1": 1, "str1": str]