AFNetworkingやAlamofireなどのライブラリでAPIを叩いて取得したjsonをオブジェクトに変換したい要求がありますよね。
Androidの場合、Retrofitを使えば簡単に上の要求を実現できます。
iOSの場合はどうでしょうか?
jsonからオブジェクトあるいはCoreDataに変換するためにMantleやOvercoatと呼ばれるライブラリがあるようです。
これらはObjective-Cの時代からあったライブラリ。
で、Mantleに関してはSwiftで動作しない問題もあるようです。
https://github.com/Mantle/Mantle/issues/344
https://github.com/Mantle/Mantle/issues/342
この辺の互換性の問題がSwift移行へのボトルネックになりそうっすね。
iOS系エンジニアの皆さんはどうしてるのでしょうか?
今回読んでみたObjectMapperやCrustがSwift版Mantleのポジションを担ってゆくことになりそう、github見てる感じ。
ということで、ObjectMapperのコードを読んでみました。
https://github.com/Hearst-DD/ObjectMapper
その過程のメモを『シェアさせていだきます』
私はSwift情弱です。
しかし、SwiftはObjective-Cより1003倍くらい読みやすくて良いですねー。
// map a JSON string onto an existing object
public func map<N: MapperProtocol>(JSON: String, to object: N) -> N! {
var json = parseJSONString(JSON)
if let json = json {
mappingType = .fromJSON
N.map(self, object: object)
return object
}
return nil
}
map
メソッドが根幹っぽい。
ちなみにこの見慣れないif let json = json { /.../ }
はoptional bindingと呼ばれる記述みたい。
変数json
を定数json
に代入した結果定数json
が存在してたら{ /.../ }
を実行する。
N
はMapperProtocol
を実装したクラス。
MapperProtocol
はmapというメソッドを備えてるので、実装された処理を実行する
同梱のObjectMapperTests.swiftでjsonからUserクラスを取り出す過程をテストしています。
UserクラスはMapperProtocolを実装してるのでmapもあります。
mapの内容は下記のとおり
class func map(mapper: Mapper, object: User) {
object.username <= mapper["username"]
object.identifier <= mapper["identifier"]
object.photoCount <= mapper["photoCount"]
object.age <= mapper["age"]
object.weight <= mapper["weight"]
object.float <= mapper["float"]
object.drinker <= mapper["drinker"]
object.smoker <= mapper["smoker"]
object.arr <= mapper["arr"]
object.arrOptional <= mapper["arrOpt"]
object.dict <= mapper["dict"]
object.dictOptional <= mapper["dictOpt"]
object.friend <= mapper["friend"]
object.friends <= mapper["friends"]
object.friendDictionary <= mapper["friendDictionary"]
object.birthday <= (mapper["birthday"], DateTransform<NSDate, Double>())
object.birthdayOpt <= (mapper["birthdayOpt"], DateTransform<NSDate, Double>())
object.imageURL <= (mapper["imageURL"], URLTransform<NSURL, String>())
}
objectのプロパティにmapperのキーに対応する値を取り出して格納してる。
はじめ見た時『え、mapper["username"]
って書いてるけど、Mapperにusernameとか無いでしょ』って思った。
これはSwiftのsubscriptという機能でした。
Mapperクラスにちゃんと定義されてた。
// Sets the current mapper value and key
public subscript(key: String) -> Mapper {
get {
// save key and value associated to it
currentKey = key
currentValue = valueFor(key)
return self
}
set {}
}
mapper["awesomeKey"]
と書くとsubscriptのget内に定義された処理が実行される。
getの中を見てみる。
受け取ったString型のkeyをcurrentKeyにセットして、valueFor(key)の結果をcurrentValueにセット、最後のMapper自身を返してる。
valueForはMapperクラスのプライベートメソッド。
受け取ったキーに対応する値をjsonから引っ張りだしてる。
getと言いつつsetしてる感。
とにかくmapper["awesomeKey"]
はMapper自身を返してる。
で、<=
これ。
どうみても比較演算子なんだけど。。。
実はこれ、独自に定義された関数でした。
https://github.com/Hearst-DD/ObjectMapper/blob/master/ObjectMapper/Core/Operators.swift で定義されてる。
public func <=<T>(inout left: T, right: Mapper) {
if right.mappingType == MappingType.fromJSON {
FromJSON<T>().baseType(&left, object: right.currentValue)
} else {
ToJSON().baseType(left, key: right.currentKey!, dictionary: &right.JSONDictionary);
}
}
Operatorsには複数の<=
が定義されています。
上に挙げた<=
はleftが基本データ型用です。
中を見てゆくと、Mapperオブジェクトのmappingタイプを見て移譲先のクラスを決定してる。
json -> objectの場合はFromJson、object -> jsonはToJsonに
処理を任せてる。
私はjson -> objectの方を追いたい。
FromJSON<T>().baseType(&left, object: right.currentValue)
そもそも呼び出し元では
object.username <= mapper["username"]
このようになっていた。
leftにはobject.usernameが、rightにはMapperオブジェクトのcurrentValueがあてがわれる。
mapper["username"]はsubscriptのgetであり、get内ではcurrentValueにusernnameでjsonから引っ張ってきた値が格納されてる。
この辺のスタックをちょいちょい確認して読まないと混乱してくる。。
FromJSON<User>().baseType(object.username, mapper["username"]のcurrentValue)
って感じ。
で、FromJSONクラスのbastTypeを見てみる。
func baseType<FieldType>(inout field: FieldType, object: AnyObject?) {
if let value: AnyObject = object {
switch FieldType.self {
case is String.Type:
field = (value as String) as FieldType
case is Bool.Type:
field = (value as Bool) as FieldType
case is Int.Type:
field = (value as Int) as FieldType
case is Double.Type:
field = (value as Double) as FieldType
case is Float.Type:
field = (value as Float) as FieldType
case is Array<CollectionType>.Type:
field = value as FieldType
case is Dictionary<String, CollectionType>.Type:
field = value as FieldType
default:
return
}
}
}
また登場したif let
。
ここではAnyObject型のvalueという変数に関数の引数objectを格納してる。
で、valueがnullじゃなかったら{}で囲われた処理が実行される。
FieldType.selfでクラス名が得られるので、この中身を基に処理を振り分けてく。
Userクラスの定義を確認すると、usernameはString型でした。
object.username
がString型だったらAnyObject型のvalueをString型にダウンキャストしてから、FieldType型(=String型)にダウンキャストしてる、なぜもう一度Stringでダウンキャストする必要があるのか分からなかった。
ここまでで、json -> objectの基本的な型へ変換する過程は分かった気がする。
ではUserクラスのプロパティとして独自のHogeクラスを持っていた場合。
baseTypeの中ではカバーできなさそうです。
先ほどOperatorには複数の<=
があると書きました。
HogeクラスがMapperProtocolを実装していれば、
// MARK:- T: MapperProtocol
public func <=<T: MapperProtocol>(inout left: T, right: Mapper) {
if right.mappingType == MappingType.fromJSON {
FromJSON<T>().object(&left, object: right.currentValue)
} else {
ToJSON().object(left, key: right.currentKey!, dictionary: &right.JSONDictionary)
}
}
これが使えそうですね。
で、FromJSONクラス内に定義されたobjectメソッドが呼ばれます。
func object<N: MapperProtocol>(inout field: N, object: AnyObject?) {
if let value = object as? [String : AnyObject] {
field = Mapper().map(value, to: N.self)
}
}
与えられたobjectがディクショナリ型にダウンキャストできたらN型でmapしてその結果をfieldにあてる、という感じ。
おお、なんとなく理解できたわ!
蛇足だけど、仮にHogeクラスの中にさらにHugaクラス(MapperProtocol実装済み)があるとする。
そうする、objectメソッドが再び呼ばれて、、、と処理が入れ子になってゆきます。
で、最終的に基本データ型に行き着いたらFromJSON.baseTypeが呼ばれて、おわり。って感じ。
今回紹介したObjectMapperのように、地味だけど開発する上で欠かせないライブラリには言語特有の処理が上手に使われてるイメージが個人的にあります。
今回のライブラリだとジェネリック型やif let(optional binding)
、as
、subscript
、inout
(特にここでは触れてなかったが)などでしょうか。
なので、何か未知の言語を学びたいという時はObjectMapping的なライブラリを読むと言語への理解が早くなります。たぶん。