RealmSwift + SwiftTask + Alamofire + ObjectMapper でサーバー上のデータをローカルDBに保存する

APIを使うとキャッシュ方法考えたり似てるけど微妙に付属データが違うレスポンスがあって辛かったりします。
「ローカルにデータが全部あってアプリのView作る時はそこから取る形にできれば楽なのに。」と思い表題の仕組みを考えてみました。

## 今回やる事
Alamofireでサーバーからデータを取得し、ObjectMapperでレスポンスをRealmのオブジェクト形式に変換・保存します。
その際、複数のAPIを叩くのでSwiftTaskでそれらを制御します。

## テーブル構造
ブログの記事一覧を表示するアプリを想定します。
テーブルはCategoryとEntryの2つを作ります。

```
■ Categoryテーブル
id: Int
name: String

■ Entryテーブル
id: Int
title: String
body: String
categoryId: Int
createdAt: NSDate
updatedAt: NSDate
```

## サーバーからのレスポンス

```:/api/v1/categories
{
  categories: [{
    "id": 1
    "name": "Swift"
  },{
    "id": 2
    "name": "Rails"
  }]
}
```

```:/api/v1/entries
{
  entries: [{
    "id": 1,
    "title": "SwiftのOptional",
    "body": "本文",
    "category_id": 1,
    "updated_at": "2015-11-04T00:00:00.000+09:00",
    "created_at": "2015-11-04T00:00:00.000+09:00"
  },{
    "id": 2,
    "title": "Swiftの配列とディクショナリ",
    "body": "本文",
    "category_id": 1,
    "updated_at": "2015-11-03T00:00:00.000+09:00",
    "created_at": "2015-11-03T00:00:00.000+09:00"
  },{
    "id": 3,
    "title": "jbuilderの書き方一覧",
    "body": "本文",
    "category_id": 2,
    "updated_at": "2015-11-02T00:00:00.000+09:00",
    "created_at": "2015-11-02T00:00:00.000+09:00"
  }
}
```

## ライブラリのインストール
ライブラリはcocoapodsでインストールします。

```rb:Podfile
use_frameworks!

pod 'Alamofire'
pod 'ObjectMapper'
pod 'RealmSwift'
pod 'SwiftTask'
```

## ローカルのModel
RealmのModelは下のように定義します。
ObjectMapperのプロトコルであるMappableも持たせます。

```category.swift
import UIKit
import RealmSwift
import ObjectMapper

class Category: Object, Mappable {
    dynamic var id = 0
    dynamic var name = ""
    
    override static func primaryKey() -> String? {
        return "id"
    }
    
    required convenience init?(_ map: Map) {
        self.init()
        mapping(map)
    }

    func mapping(map: Map) {
        id   <- map["id"]
        name <- map["name"]
    }
}
```

```entry.swift
import UIKit
import RealmSwift
import ObjectMapper

class Entry: Object {
    dynamic var id = 0
    dynamic var title = ""
    dynamic var body = ""
    dynamic var categoryId = 0
    dynamic var updatedAt = NSDate()
    dynamic var createdAt = NSDate()
    
    override static func primaryKey() -> String? {
        return "id"
    }
    
    required convenience init?(_ map: Map) {
        self.init()
        mapping(map)
    }
    
    func mapping(map: Map) {
        let formatter = NSDateFormatter()
        formatter.dateFormat = "YYYY-MM-dd'T'HH:mm:ss.SZ"
        let dateFormatterTransform = DateFormatterTransform(dateFormatter: formatter)
        
        id         <- map["id"]
        title      <- map["title"]
        body       <- map["body"]
        categoryId <- map["category_id"]
        updatedAt  <- (map["updated_at"], dateFormatterTransform)
        createdAt  <- (map["created_at"], dateFormatterTransform)
    }
}
```

## 通信・DBへの保存処理
AlamofireでAPIを叩いてRealmに保存する処理を書きます。

```CategoryEndPoint.swift
import UIKit
import Alamofire
import ObjectMapper
import RealmSwift

class CategoryEndPoint {
    func fetch() {
        Alamofire.request(.GET, "http://localhost:3000/api/v1/categories").responseJSON { response in
            guard let json = response.result.value as? Dictionary<String, Array<AnyObject>> else {
                return
            }
            
            json["categories"]?.forEach {
                guard let category = Mapper<Category>().map($0) else {
                    return
                }
                
                do {
                    let realm = try Realm()
                    try realm.write {
                        // add:update:は同じIDのものがあればUpdate、なければInsertしてくれるメソッド
                        realm.add(category, update: true)
                    }
                } catch {}
            }
        }
    }
}
```

```EntryEndPoint.swift
import UIKit
import Alamofire
import ObjectMapper
import RealmSwift

class EntryEndPoint {
    func fetch() {
        Alamofire.request(.GET, "http://localhost:3000/api/v1/entries").responseJSON { response in
            guard let json = response.result.value as? Dictionary<String, Array<AnyObject>> else {
                return
            }
            
            json["entries"]?.forEach {
                guard let entry = Mapper<Category>().map($0) else {
                    return
                }
                
                do {
                    let realm = try Realm()
                    try realm.write {
                        realm.add(entry, update: true)
                    }
                } catch {}
            }
        }
    }
}
```

AppDelegateを下のように書けばアプリ起動時にキャッシュを更新してくれます。

```AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func applicationDidBecomeActive(application: UIApplication) {
        CategoryEndPoint().fetch()
        EntryEndPoint().fetch()
    }
}
```

## SwiftTaskでデータ更新が終わったタイミングの処理を書く
今のままだと全データを更新後の処理を書けません。
SwiftTaskを使って全てのデータ更新が終わったタイミングで処理を書けるようにします。

CategoryEndPointとEntryEndPointのfetchを修正してTaskを返すメソッドにします。

```CategoryEndPoint.swift
import UIKit
import Alamofire
import ObjectMapper
import RealmSwift
import SwiftTask

class CategoryEndPoint {
    func fetchTask() -> Task<Float, String, NSError?> {
        return Task<Float, String, NSError?> { progress, fulfill, reject, configure in
            Alamofire.request(.GET, "http://localhost:3000/api/v1/categories").responseJSON { response in
                guard let json = response.result.value as? Dictionary<String, Array<AnyObject>> else {
                    reject(nil)
                    return
                }
                
                json["categories"]?.forEach {
                    guard let category = Mapper<Category>().map($0) else {
                        return
                    }
                    
                    do {
                        let realm = try Realm()
                        try realm.write {
                            realm.add(category, update: true)
                        }
                    } catch {}
                }
                fulfill("SUCCESS")
            }
        }
    }
}
```

```EntryEndPoint.swift
import UIKit
import Alamofire
import ObjectMapper
import RealmSwift
import SwiftTask

class EntryEndPoint {
    func fetchTask() -> Task<Float, String, NSError?> {
        return Task<Float, String, NSError?> { progress, fulfill, reject, configure in
            Alamofire.request(.GET, "http://localhost:3000/api/v1/entries").responseJSON { response in
                guard let json = response.result.value as? Dictionary<String, Array<AnyObject>> else {
                    reject(nil)
                    return
                }
                
                json["entries"]?.forEach {
                    guard let entry = Mapper<Category>().map($0) else {
                        return
                    }
                    
                    do {
                        let realm = try Realm()
                        try realm.write {
                            realm.add(entry, update: true)
                        }
                    } catch {}
                }
                fulfill("SUCCESS")
            }
        }
    }
}
```

AppDelegateはSwiftTaskを使ってデータの更新を行うようにします。

```AppDelegate.swift
import UIKit
import SwiftTask

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    func applicationDidBecomeActive(application: UIApplication) {
        resetCache()
    }
}

extension AppDelegate {
    private func resetCache() {
        let tasks = [CategoryEndPoint().fetchTask(), EntryEndPoint().fetchTask()]
        
        Task.all(tasks).success { _ in
            // 全部の通信が完了したタイミングでここに来る
            }.failure { _ in
                // どれかの処理が失敗したらここに来る
        }
    }
}
```

## アプリ上でデータを参照する
アプリからはRealmを使ってデータを取得できるようになりました。
Realmで書かれているので検索やソートも簡単に行なえます。

```ViewController.swift
import UIKit
import RealmSwift

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        do {
            let realm = try Realm()
            print(realm.objects(Category)) // カテゴリー一覧を取得
        } catch {}
    }
}
```

## サーバー側でデータが消される場合
今の実装ではサーバー側で削除されたデータを検知できません。
サーバー側でデータを削除する事がある、場合はAPIを叩いた後に一度ローカルのデータを全削除する方が良いです。

```swift
// 一度全件削除する
do {
    let realm = try Realm()
    try realm.write {
        realm.objects(Category).forEach { realm.delete($0) }
    }
} catch {}

json["categories"]?.forEach {
    guard let category = Mapper<Category>().map($0) else {
        return
    }
                    
    do {
        let realm = try Realm()
        try realm.write {
            realm.add(category)
        }
    } catch {}
}
```

## テーブル構造が変更された時
テーブル構造が変わった時は該当Modelを変更して`Realm.Configuration(schemaVersion: 1)`のschemaVersionバージョンを更新するだけで対応できます。
RealmはCoreDataと比べてマイグレーションが格段に楽になったのが嬉しいです。

```swift
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
        
        // テーブル構造が変わったらschemaVersionを2、3と増やしていけば良い
        let config = Realm.Configuration(schemaVersion: 1)
        Realm.Configuration.defaultConfiguration = config
        
        return true
    }
}
```

## 参考URL
http://qiita.com/SatoshiN21/items/d09f9e028c129ef567c1

■ 2018/6/24追記

Swift4以降を使っている場合、サーバーからデータを取得してオブジェクト化する部分は`Alamofire` + `ObjectMapper`ではなく`Alamofire` + `Codable`という組み合わせがオススメです。
詳しくは下記事でまとめたので良かったらこちらも参考にしてみてください。

[Alamofire+CodableでAPIのレスポンスをオブジェクトに変換、Swift4.1対応版](http://www.cl9.info/entry/2017/10/06/134510)