[2017年版]RxSwift + Alamofire + ObjectMapper + RealmのSwift実装について

  • 30
    いいね
  • 0
    コメント

はじめに

タイトルに記載したテクノロジーを使って、APIでデータを取得してRealmに保存するコードを残しておきます。

全体を通したコードが毎年変わるので、現時点で作成しているコードという意味で2017年版と書いてます。
これは2017年の決定版という意味ではありません。

なお、記事の内容は長いですが全部読めば多分理解できると思うように書いてます。

注意

以下のプログラムは動作検証をしていないため、正しく動かないケースがあるかもしれません。
しかし、もし動作しなくてもXCodeのコンパイラが出力したエラーの通り行えば動くようになるはずです。
(コメントで指摘してもらえると幸いです。)

各テクノロジーの簡単な説明

RxSwift

僕の中では必須といえるテクノロジーなのですが、説明が難しいテクノロジーです。
RxSwiftの全容ではないですが、RxSwiftはObserverパターンを簡単に扱える技術です。

例えば何らかのアクションが発生したときにヘッダーフッターに対して通知したいときなどに使えます。
例)「記事を読んだ時にヘッダーの未読記事数をひとつ減らしフッターのバッジも一つ減らす。」のような何らかのアクションをトリガーに複数の処理が実行する場合などに使えます。

本記事ではAPIでデータを取得した際の結果を通知する時に使用します。

Alamofire

手軽にAPIリクエストを発行できるテクノロジーです。
簡単なアプリならこれだけでAPI通信はバッチリできるでしょう。

ObjectMapper

Jsonデータをオブジェクトに詰め込む技術です。
今回はAPIで取得したJSONデータをオブジェクトに詰め込みます。

本記事ではObjectMapperをラップしているAlamofireObjectMapperを使用してます。

Realm

しばらくの間はデータ保存のテクノロジーというとRealmが代表になると言っても過言ではないものです。
ただし、継承周りに癖があったり、Writeトランザクション内でしかデータの書き換えが出来ない、スレッドを跨げないなどの特徴もあるので万能では無いです。

インストール

僕はライブラリのインストールをCarthageで行っていますが、CocoaPodsでも問題ないです。
Swift Package Managerはまだ使ったことがないです・・・)

github "ReactiveX/RxSwift"
github "realm/realm-cocoa"
github "Alamofire/Alamofire" ~> 4.4
github "Alamofire/AlamofireImage" ~> 3.1
github "Hearst-DD/ObjectMapper" ~> 2.2
github "tristanhimmelman/AlamofireObjectMapper" ~> 4.0

AlamofireImageに関しては最後に軽く紹介しています。
大抵は画像を扱うと思うので、使ったほうが便利だと思います。

インストール方法は割愛。

データの取得

今回はAPIで以下の都道府県を取得するケースを例にします。

[Get]/api/pref.json
[{
  "id": 1,
  "name": "北海道",
  "areas": [
    {
      "name": "札幌",
      "code": "sapporo"
    },
    {
      "name": "函館",
      "code": "hakodate"
    }
  ]
},
{
...
}]

Alamofireを使ってこのデータを取得するためには以下のようにアクセスします。

import Alamofire

...

Alamofire.request("http://localhost/api/pref.json").responseJSON { response in
    print(response.request)  // original URL request
    print(response.response) // HTTP URL response
    print(response.data)     // server data
    print(response.result)   // result of response serialization

    if let JSON = response.result.value {
        print("JSON: \(JSON)")
    }
}

githubより抜粋ですが、この方法で問題なくデータが取得できます。

格納するモデルの作成

次にデータを格納するモデルを作成します。

import ObjectMapper

class Prefecture: Mappable{
    // 都道府県名
    dynamic var name = ""

    required init?(map: Map) {}

    // Mappable
    func mapping(map: Map) {
        name <- map["name"]
    }
}

エリアデータも格納する必要がありますが、複雑になるため後述します。

Alamofire+ObjectMapper

取得したデータを先程作成したモデルに格納します。

import Alamofire
import ObjectMapper
import AlamofireObjectMapper

...

Alamofire.request("http://localhost/api/pref.json").responseArray() { (response: DataResponse<[Prefecture]>) in
  switch response.result {
  case .success(let prefectures):
    // データ取得成功([Prefectures]としてデータが取得できてます。)
    print("Prefectures: \(prefectures)")
  case .failure(let error):
    // データ取得エラー
  }
}

これで成功時に都道府県リストが取得できます。
もし一件のデータを取得したい場合は以下のように行います。

// [変更] responseArray->responseObjectに変更して[Prefecture]->Prefectureになっている。
Alamofire.request("http://localhost/api/pref.json").responseObject() { (response: DataResponse<Prefecture>) in
  switch response.result {
  case .success(let prefecture):
    // データ取得成功
    print("Prefecture: \(prefecture)")
  case .failure(let error):
    // データ取得エラー
  }
}

また、データが以下のようなパス構造になっている場合は以下のように行います。

[Get]/api/pref.json
{ "pref": [{
  "id": 1,
  "name": "北海道",
  "areas": [
    {
      "name": "札幌",
      "code": "sapporo"
    },
    {
      "name": "函館",
      "code": "hakodate"
    }
  ]
},
{
...
}]}

keyPathを設定して取得します。

Alamofire.request("http://localhost/api/pref.json").responseArray(keyPath: "pref") { (response: DataResponse<[Prefecture]>) in
  switch response.result {
  case .success(let prefectures):
    // データ取得成功
    print("Prefectures: \(prefectures)")
  case .failure(let error):
    // データ取得エラー
  }
}

データを保存します。

次は取得してきたデータをRealmに保存します。
Realmに保存するためにはモデルを以下のように少し変える必要があります。

// [変更]RealmSwiftを追加する。
import RealmSwift
import ObjectMapper

// [変更]Objectクラスを継承する。
class Prefecture: Object, Mappable {
    // 都道府県名
    dynamic var name = ""

    // [変更]この部分を以下のように変更する。
    required convenience init?(map: Map) {
        self.init()
    }

    // Mappable
    func mapping(map: Map) {
        name <- map["name"]
    }
}

モデルを変更した後にAPI取得後の部分を変更します。

Alamofire.request("http://localhost/api/pref.json").responseArray() { (response: DataResponse<[Prefecture]>) in
  switch response.result {
  case .success(let prefectures):
    // データ取得成功したの保存します。
    let realm = try! Realm()
    try! realm.write {
      // 「update: true」のオプションに関しては今回は割愛します。
      realm.add(prefectures)
    }
  case .failure(let error):
    // データ取得エラー
  }
}

エリアデータの保存

ここまでで都道府県データは保存できますが、エリアデータを保存することができません。
エリアデータを保存するためには一工夫が必要です。

まずはエリアデータを格納するクラスを作成します。

import RealmSwift
import ObjectMapper

class Area: Object, Mappable {
    // エリア名
    dynamic var name = ""
    // エリアコード
    dynamic var code = ""

    required convenience init?(map: Map) {
        self.init()
    }

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

ObjectMapperでは基本型データは以下のように簡潔にマッピングできるようになっています。

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

しかし、日付型等の場合は自分でトランスファーを行う必要があります。
下記は日付型の例になります。
(Railsを使っている場合は以下の形式でほぼ問題ないと思いますが、サーバーのレスポンスに合わせて変更する必要があります。)

func mapping(map: Map) {
    date <- (map["date"], CustomDateFormatTransform(formatString: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"))
}

今回のように自分で作成したAreaクラスにデータを入れる場合は、上記のトランスファーを自作する必要があります。
僕はここでつまづきました。
こちらのページを見つけたことで、解決したので転記します。

まずは都道府県クラスを書き換えます。

import RealmSwift
import ObjectMapper

class Prefecture: Object, Mappable{
    // 都道府県名
    dynamic var name = ""
    // [変更]エリアを複数形で定義
    var areas = List<Area>()

    required convenience init?(map: Map) {
        self.init()
    }

    func mapping(map: Map) {
        name <- map["name"]
        // [変更]エリアを追加
        area <- (map["areas"], ArrayTransform<Area>())
    }
}

先程のページからArrayTransformをコピーします。

class ArrayTransform<T:RealmSwift.Object> : TransformType where T:Mappable {
    typealias Object = List<T>
    typealias JSON = Array<AnyObject>

    let mapper = Mapper<T>()

    func transformFromJSON(_ value: Any?) -> List<T>? {
        let result = List<T>()
        if let tempArr = value as! Array<AnyObject>? {
            for entry in tempArr {
                let mapper = Mapper<T>()
                let model : T = mapper.map(JSON: entry as! [String : Any])!
                result.append(model)
            }
        }
        return result
    }

    func transformToJSON(_ value: Object?) -> JSON? {
        var results = [AnyObject]()
        if let value = value {
            for obj in value {
                let json = mapper.toJSON(obj)
                results.append(json as AnyObject)
            }
        }
        return results
    }
}

このクラスをパスの通っているとこに定義すれば完成です。

注意

Prefectureクラスを書き換えたのでRealmのRealmSchemaVersionを変更するかアプリを入れ直さないとエラーが発生します。
RealmCoreDataとは違いマイグレーションが簡単に行えます。Realmが好まれる理由の一つにこれが含まれます。
それでもスキーマの変更を自動的に行うことはなく、RealmSchemaVersionが変更された場合に自動的に行うようになってます。
(クラスごとにスキーマを持っているため、クラスを追加した場合はRealmSchemaVersionを変更しなくても動きます。)

取得後について

ここまででデータの取得と保存ができていますが、このままでは取得したデータを使うのに手間がかかります。
例えば、コールバックを使うとした場合は以下のようになります。

func request(callback:([Prefecture])->()) {
  Alamofire.request("http://localhost/api/pref.json").responseArray() { (response: 
  DataResponse<[Prefecture]>) in
    switch response.result {
    case .success(let prefectures):
      // データ取得成功したの保存します。
      let realm = try! Realm()
      try! realm.write {
        // 「update: true」のオプションに関しては今回は割愛します。
        realm.add(prefectures)
      }
      callback(prefectures)
    case .failure(let error):
      // データ取得エラー
    }
  }
}

上記のようなコードを書いて

request() { (prefectures) in
  print(prefectures)
}

簡単なアプリケーションを作るならコールバックを使って構築してもそんなに問題になることはありません。
しかし、APIを同時に2本走らせて両方の結果を待つ場合など複雑なことをやろうとした場合にコールバック処理で構築しているとコールバック地獄に陥る可能性があります。
RxSwiftはそんな時に力を発揮するテクノロジーです。

RxSwiftの導入

今回は踏み込んで書きませんが、RxSwiftObserverパターンを使って処理を伝播させます。
RxSwiftのコードが完成すれば、GitHubに書かれているサンプルの処理がほぼ全て行える事になります。

僕の経験上では、複数APIの待ち受けやAPIの結果を元に次のAPIを実行する場合などに重宝しました。

先程のコールバックパターンをRxSwiftに置き換えます。

// [変更] 戻り値を追加
func request() -> Observable<[Prefecture]> {
  // [変更] Observableを返却
  return Observable.create { (observer: AnyObserver<[Prefecture]>) in
    Alamofire.request("http://localhost/api/pref.json").responseArray() { (response: 
    DataResponse<[Prefecture]>) in
      switch response.result {
      case .success(let prefectures):
        // データ取得成功したの保存します。
        let realm = try! Realm()
        try! realm.write {
          // 「update: true」のオプションに関しては今回は割愛します。
          realm.add(prefectures)
        }

        // [変更] 通知の処理は「observer」に任せる。
        observer.on(.next(prefectures))
        observer.onCompleted()
      case .failure(let error):
        // データ取得エラー

        // [変更] 通知の処理は「observer」に任せる。
        observer.onError(error)
      }
    }
    return Disposables.create()
  }
}

ObservableObserverが通知した情報を受取る仕組みです。
Observableを受け取るため、使用する側の処理は以下のようになります。

request().subscribe(onNext: { (prefectures) in
  print(prefectures)
}, onError: {...})

subscribeを行うことで、Observableから通知を受け取れるようにします。
RxSwiftではデータの伝播をObservableを通じて行うので、インターフェースの統一ができます。
これでRxSwiftの対応はおしまいです。

ちょっと古い記事ですが、RxSwiftのメモリ管理についてはこちらに記事を書いているのでよければみてください。

おまけ その1

マスターデータを取得する場合に、キャッシュを使う場合もあると思います。
以下コードの「キャッシュがある場合」の箇所にキャッシュ保持時間の条件を入れれば「キャッシュがあればキャッシュを返し、なければAPIからデータを取得して返す」という処理になります。

func request() -> Observable<[Prefecture]> {
  return Observable.create { (observer: AnyObserver<[Prefecture]>) in
    // [追加] キャッシュがある場合
    if キャッシュがある場合 {
      observable.on(.next(Prefecture.all()))
      observable.onCompleted()
      return Disposables.create()
    }

    Alamofire.request("http://localhost/api/pref.json").responseArray() { (response: 
    DataResponse<[Prefecture]>) in
      switch response.result {
      case .success(let prefectures):
        // データ取得成功したの保存します。
        let realm = try! Realm()
        try! realm.write {
          // [変更]エリアデータは自動的に消えないため、手動で削除する。
          realm.delete(realm.objects(Area.self))
          realm.add(prefectures, update: true)
        }

        observer.on(.next(prefectures))
        observer.onCompleted()
      case .failure(let error):
        // データ取得エラー
        observer.onError(error)
      }
    }
    return Disposables.create()
  }
}

呼び出し元は特に変更する必要はありません。

おまけ その2

APIの処理と描画処理はスレッドが別になります。
なので、APIのデータが戻ってきた後に処理をメインスレッドに渡す必要があります。
RxSwiftを使う場合は以下の記載でスレッドを合わせることができます。

request().observeOn(MainScheduler.instance).subscribe(onNext: { (prefectures) in
  // UIを変更する
}, onError: {...})

observeOn(MainScheduler.instance)を追加するだけでメインスレッドとして処理できるようになります。

おまけ その3

Alamofireに共通ヘッダーをつける時に以下のようにしておくと便利です。

    func createRequest(url:String, parameters: Parameters? = nil) -> Alamofire.DataRequest {
        let manager = Alamofire.SessionManager.default
        // タイムアウト
        manager.session.configuration.timeoutIntervalForRequest = 300

        return manager.request(url,
            // メソッド
            method:.get,
            parameters: parameters,
            encoding: JSONEncoding.default,
            // 共通ヘッダー
            headers: ["Accept": "application/json"]).validate()
    }

// 使用例
createRequest(url:"http://localhost/api/pref.json").responseArray...

おまけ その4

あと、ここには書いていませんが画像取得はAlamofireImageが現時点では良さそうです。
(使った感じので、他と比較したわけではありません。)

import AlamofireImage

// 省略

// ただ画像を表示する。
imageView.af_setImage(withURL: Url())

// 画像を表示できない場合に代替画像を入れたり、画像ロード後にリサイズしたりしたい場合
imageView.af_setImage(withURL: Url(), placeholderImage: UIImage(named: "DefaultImage"), completion: { (image) in
    if let image = image.value {
      // 表示後に何かしたい場合
    }
})

何か難しいことをしないのであれば、上記のコードで使えるのでお手軽です。
画像のキャッシュや管理もしてくれるのでおすすめです。

おまけ その5

管理画面を作る場合はEurekaを使うと良いです。
これは公式ページにサンプルがあるので、cloneして確認しながら使うと良いです。
大抵のことができる上に、エンジニアでも綺麗な管理画面が作れるのでおすすめです。

これは本当に関係ないおまけ・・・ですねw

最後に

この記事が誰かの役に立てれば幸いです。