SwiftでjsonをCoreData(NSManagedObject)にマッピングするならEasyMappingがいいんじゃないの？


# 背景

サーバーAPIで返ってくるjsonを、CoreDataのオブジェクトにマッピングして保存したかった。
今回いくつかライブラリを検討した結果、[EasyMapping](https://github.com/EasyMapping/EasyMapping)が利用しやすそうだったので、採用することにした。

# EasyMappingとCoreDataの連携方法

基本的なEasyMappingの使い方はgithubのREADMEを見るのが一番分かりやすいと思う。
[CocoaPodsでもインストールできる](https://github.com/EasyMapping/EasyMapping#installation)し、[Swiftでの利用例](https://github.com/EasyMapping/EasyMapping/wiki/Swift-and-EasyMapping)も公式から提供されている。

ただ、CoreDataオブジェクトとのマッピングの例はほとんどなかったのでここではそちらについて書こうと思う。

## エンティティの定義

CoreDataで利用するエンティティは普通`NSManagedObject`のサブクラスとなるが、EasyMappingに対応するには`EKManagedObjectModel`を継承する。

今回のサンプルでは3つのプロパティを持った以下のようなエンティティを定義した。

```swift:Profile.swift
import Foundation
import CoreData

@objc(Profile)
class Profile: EKManagedObjectModel {

    @NSManaged var id: NSNumber
    @NSManaged var userName: String
    @NSManaged var birthday: NSDate

}
```

マッピングの定義は`objectMapping`メソッドをオーバーライドする。

```swift:Profile+Methods.swift
import Foundation

extension Profile {

    override class func objectMapping() -> EKManagedObjectMapping {
        // マッピングの定義
        var mapping = EKManagedObjectMapping(entityName: "Profile")
        
        // jsonのkeyと同じプロパティ名のマッピング
        mapping.mapPropertiesFromArray(["id"])
        
        // jsonのkeyと異なるプロパティ名のマッピング(key名 -> property名)
        mapping.mapPropertiesFromDictionary(["user_name":"userName"])
        
        // NSDateのマッピング
        var formatter: NSDateFormatter = NSDateFormatter()
        formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
        formatter.dateFormat = "yyyy-MM-dd"
        mapping.mapKeyPath("birthday", toProperty: "birthday", withDateFormatter: formatter)
        
        return mapping        
	}

}
```

## マッピングの実行

上記のような準備をすれば、以下のようなサンプルが実行できる。


```swift:sample.swift
func sample(context: NSManagedObjectContext) {
    
    // マッピングしたいDictionary
    let json: [String:AnyObject] = [
        "id": 1234,
        "user_name": "my name",
        "birthday": "2007-08-31"
    ]
    
    // Dictionaryからエンティティを作成
    let prof: Profile = Profile.objectWithProperties(json, inContext: context)
    println("id: \(prof.id)")             // id: 1234
    println("userName: \(prof.userName)") // userName: my name
    println("birthday: \(prof.birthday)") // birthday: 2007-08-31 00:00:00 +0000
    
    // エンティティからDictionaryを作成
    let dic: AnyObject = prof.serializedObjectInContext(context)
    println(dic)
    // {
    //     birthday = "2007-08-31";
    //     id = 1234;
    //     "user_name" = "my name";
    // }
}
```

「jsonからエンティティの作成」・「エンティティからjsonの作成」が、それぞれ一行だけで簡単に記述できた。
これはマッピングをモデルクラス内で定義しているおかげ。

```swift:
// Dictionaryからエンティティを作成
let prof: Profile = Profile.objectWithProperties(json, inContext: context)
```

```swift:
// エンティティからDictionaryを作成
let dic: AnyObject = prof.serializedObjectInContext(context)
```

# 応用編

## 1. エンティティの更新

先の例のように`objectWithProperties`を利用すると、常に新しいエンティティが作成されてしまう。
既に存在するエンティティのプロパティを更新したい場合は、`EKManagedObjectMapper.fillObject`を利用すれば良い。

```swift:
        let json1: [String:AnyObject] = ["user_name": "new name"]
        EKManagedObjectMapper.fillObject(prof, fromExternalRepresentation: json1,
            withMapping: Profile.objectMapping(), inManagedObjectContext: context)
        println(prof.serializedObjectInContext(context))
        // [id: 1234, user_name: new name, birthday: 2007-08-31]

```

## 2. カスタムマッピング

もっと複雑なマッピングを行うこともできる。
Bool型のis_publicというkeyの値を、NSNumber型のisPublicプロパティにマッピングする例を示す。

```
        // BoolをNSNumberにマッピング
        mapping.mapKeyPath("is_public", toProperty: "isPublic",
            withValueBlock: { (key, value, context) -> AnyObject! in
                return value
        }) { (value, context) -> AnyObject! in
            return value.boolValue // NSNumberをBoolに変換
        }
```

## 上記2点を反映したサンプル

```swift:Profile.swift
import Foundation
import CoreData

@objc(Profile)
class Profile: EKManagedObjectModel {

    @NSManaged var userName: String
    @NSManaged var id: NSNumber
    @NSManaged var birthday: NSDate
    @NSManaged var isPublic: NSNumber

}
```

```swift:Profile+Methods.swift
import Foundation

extension Profile {
    
    override class func objectMapping() -> EKManagedObjectMapping {
        // マッピングの定義
        var mapping = EKManagedObjectMapping(entityName: "Profile")
        
        // jsonのkeyと同じプロパティ名のマッピング
        mapping.mapPropertiesFromArray(["id"])
        
        // jsonのkeyと異なるプロパティ名のマッピング(key名 -> property名)
        mapping.mapPropertiesFromDictionary(["user_name":"userName"])
        
        // NSDateのマッピング
        var formatter: NSDateFormatter = NSDateFormatter()
        formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
        formatter.dateFormat = "yyyy-MM-dd"
        mapping.mapKeyPath("birthday", toProperty: "birthday", withDateFormatter: formatter)
        
        // BoolをNSNumberにマッピング
        mapping.mapKeyPath("is_public", toProperty: "isPublic",
            withValueBlock: { (key, value, context) -> AnyObject! in
                return value
        }) { (value, context) -> AnyObject! in
            return value.boolValue
        }
        
        return mapping
    }
    
}
```

```swift:
func sample(context: NSManagedObjectContext) {
    
    // マッピングしたいDictionary
    let json: [String:AnyObject] = [
        "id": 1234,
        "user_name": "my name",
        "birthday": "2007-08-31",
        "is_public": true
    ]
    
    // Dictionaryからエンティティを作成
    let prof: Profile = Profile.objectWithProperties(json, inContext: context)
    println("id: \(prof.id)")             // id: 1234
    println("userName: \(prof.userName)") // userName: my name
    println("birthday: \(prof.birthday)") // birthday: 2007-08-31 00:00:00 +0000
    println("isPublic: \(prof.isPublic)") // isPublic: 1
    
    // エンティティからDictionaryを作成
    let dic: AnyObject = prof.serializedObjectInContext(context)
    println(dic)
    // {
    //     birthday = "2007-08-31";
    //     id = 1234;
    //     "is_public" = 1;
    //     "user_name" = "my name";
    // }

	// エンティティの更新
    let json1: [String:AnyObject] = ["user_name": "new name", "is_public": false]
    EKManagedObjectMapper.fillObject(prof, fromExternalRepresentation: json1,
        withMapping: Profile.objectMapping(), inManagedObjectContext: context)
    println(prof.serializedObjectInContext(context))
	// [id: 1234, is_public: 0, birthday: 2007-08-31, user_name: new name]
    
}
```

# 感想

マッピングをモデルクラス側に記述できるのは管理がしやすくて良い。しかもエンティティの生成などには、暗黙的にマッピングを利用してくれる。結果として利用する側のソースが簡潔に書ける。
カスタムマッピングの機能もあるので、かなり柔軟にマッピングが実装できる点も重要だ。特にCoreDataの場合は、relationを利用している場合などに重宝しそうである。

現状プロジェクトでは、サーバーとの通信周りは[Alamofire](https://github.com/Alamofire/Alamofire)、CoreDataの操作には[MagicalRecord](https://github.com/magicalpanda/MagicalRecord)を利用しているがEasyMappingとの併用に関して全く支障はない。

# 検討したオブジェクトマッピングライブラリ

EasyMapping以外に今回候補に上がったライブラリと、検討時のメモ。
実際には利用していないものもあるので、もしかするとEasyMappingより良いものがあるかもしれない。

* [Mantle/Mantle](https://github.com/Mantle/Mantle)  
Objective-Cではよく利用されているライブラリ。  
ただ、Swiftでは動かないという情報もあるので今回は避けたかった。
* [dchohfi/KeyValueObjectMapping](https://github.com/dchohfi/KeyValueObjectMapping)  
公式以外情報ないが、利用は簡単そう。
* [Hearst-DD/ObjectMapper](https://github.com/Hearst-DD/ObjectMapper)  
Swiftで書かれているようなのでSwiftで利用するなら安心か。  
でもNSManagedObject(CoreData)で利用できるかは分からない。
* [EasyMapping/EasyMapping](https://github.com/EasyMapping/EasyMapping)  
最終更新日が新しいのでメンテナンスはされていそう。   
CoreDataにも対応してるっぽいし、試してみよう。
* [mutualmobile/MMRecord](https://github.com/mutualmobile/MMRecord)  
サーバーへのリクエストも含めて利用できる。  
SwiftやCoreDataの例も載ってるが、正しく動かないというIssueが上がってるのが懸念点。

# 参考

EasyMapping/EasyMapping
https://github.com/EasyMapping/EasyMapping

dchohfi/KeyValueObjectMapping
https://github.com/dchohfi/KeyValueObjectMapping

Hearst-DD/ObjectMapper
https://github.com/Hearst-DD/ObjectMapper

Mantle/Mantle
https://github.com/Mantle/Mantle

mutualmobile/MMRecord
https://github.com/mutualmobile/MMRecord

Alamofire/Alamofire · GitHub
https://github.com/Alamofire/Alamofire