126
123

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ReSwiftとRealmが最高に相性がいいっていう話

Last updated at Posted at 2016-05-03

はじめに

近々リリース申請に出そうと思っている新作アプリでReSwiftとRealmを使ったのですが結構相性がいいなと思ったので基本的な考え方と少し躓いたところなどを共有したいと思います。

ReSwiftって?

Realmはもちろん有名なので先人の素晴らしい記事に解説をまかせるとしてReSwiftについて簡単に説明したいと思います。

ReSwiftはReduxというJavaScriptのフレームワークをSwiftで実装したものです。ReSwiftやReduxは以下の四つの要素で成り立っています。

  • View ... iOSアプリだとViewControllerに対応、画面に表示する部分
  • Action ... アプリ内で行われる処理。ただしこれ単体ではなにもしない
  • State(Store) ... アプリの状態を保持/表現するもの
  • Reducer ... 現在のStateとActionを受け取って新しいStateを返す部分

詳しいことは公式のREADMEをみてください。また勉強の傍ら僕が日本語訳も順次しているのでよかったら活用してください。

相性のいいわけ

さて、ReSwiftの簡単な説明が済んだところでなぜRealmと「最高に相性がいい」というのかというところを説明したいと思います。

その理由はReSwiftのStateという概念にあります。ReSwiftを使ったアプリ設計においてはアプリの状態は全てStateで一括管理されます。そしてこのStateはStateTypeというプロトコルを採用した構造体で表されます。
つまりStateに該当するのは専用のプロトコルを採用したただの構造体なのでRealmのList<Object>型の変数なども定義できてしまいます。つまり、ちょっと工夫するだけでアプリの状態をまるごとRealmに保存できるのです。

ReSwift+Realmでアプリの状態を保存する

import文を省略してとても簡単な例でReSwift+Realmのアプリ例を考えてみましょう。ReSwiftのドキュメントにそってCountアプリでいきたいと思います。

ViewController.swift
// ViewController
class ViewController: UIViewController, StoreSubscriber {
    @IBOutlet var countLabel: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Realmからの読み込みはinitialViewControllerのviewDidLoadの中だけ
        let realm = try! Realm()
        let rstore = realm.objects(StoreState).first ?? StoreState()
        store.state.count = rstore.count
        store.subscribe(self)
    }
    func newState(state: AppState) {
        countLabel.text = "\(state.count)"
    }
    @IBAction func plus() {
        store.dispach(PushPlusButton())
    }
    @IBAction func minus() {
        store.dispach(PushMinusButton())
    }
}
Action.swift
// Actionはこのように空の構造体で定義します
struct PushPlusButton: Action {}
struct PushMinusButton: Action {}
// 引き数を取りたい場合は以下のようにします
// struct SampleAction: Action {
//     let sample: Int
// }
AppState.swift
struct AppState: StateType {
    var count: Int?
}
// アプリの状態は更新されていくものなのでプライマリキーを設定しておきます。
// 残念ながらObjectを継承したままStateTypeを採用することはできませんでした。
class StoreState: Object {
    dynamic var id = 0
    dynamic var count = 0
    override static func primaryKey() -> String? {
        return "id"
    } 
}
Reducers.swift
struct AppReducer: Reducer {
    func handleAction(action: Action, state: AppState?) {
        var state = state ?? AppState()
        // 一応この時点でRealmを定義しておきます。
        let realm = try! Realm()
        switch action {
        case _ as PushPlusButton:
            if state.count == nil {
                state.count = 0
            }
            state.count += 1
            save()
        case _ as PushMinusButton:
            if state.count == nil {
                state.count = 0
            }
            state.count -= 1
            save()
        // 引数をとった場合は以下のように
        // case let action as SampleAction:
        //    print(action.sample)
        }
    }
    func save() {
        // アプリの状態が変わるのはこのタイミングだけなのでRealmの更新はここのみです。
        let rstore = StoreState()
        rstore.count = state.count ?? 0
        try! realm.write() {
            realm.add(rstore, update: true)
        }
    }
}
AppDelegate.swift
// ドキュメントにのってないのでわかりにくいのですがこの一文がReSwiftにはかならず必要です。
var store = Store<AppState>(reducer: AppReducer(), state: nil)

このようにRealmを扱う部分が二箇所で済んで、かつ役割分担がはっきりしているのでデバッグもしやすいです。

躓いたところ

最後に躓いたところを書いて終わりにしたいと思います。

List<Object>を使う時

まず一番にList<Object>を扱う時に気をつけなければいけません。stateの変数であるList<Object>に要素を追加する分にはいいのですが

state.list[2].count -= 2

例えばこのような処理をそのまま書くとRealmのエラーで弾かれます。state自体はRealmとは関係なく変更できるのですがListの要素になった途端ObjectがRealmに保存されているObjectとして扱われてしまうのです。よって次のようにします。

try! realm.write() {
    state.list[2].count -= 2
}

またこの仕様のためList<Object>に追加する要素は全て別のインスタンスでなければいけません。同じインスタンスを何度も追加するとそのうちの一個を更新したとき同じインスタンスの全てが更新されてしまいます。

nilを扱えるReSwiftと扱えないRealm

ReSwiftのStateTypeを採用した構造体とRealmのObject型の決定的な違いがnilの扱いです。RealmはObjective-Cの機能を継承するのでこのようになっているのですが、この違いがデバッグしにくい原因を生んでいます。
ReSwiftのライフサイクルの順番がわかりにくいというのも一因なのですがどこでstateが最初に呼び出され、Realmのデータを読み込むviewDidLoadはどのタイミングなのかというのが把握するのがなかなか難しいので仮の初期値をつくっておいて??演算子などでうまく対応するのが無難になると思います。

というかstateがnilなのにRealmに書き込まなきゃいけないという場面は多々あるので??演算子はうまく利用しましょう。

⇒Realm1.0.0になってどうやらRealmでもnilが使えるようになったみたいですね

その他

その他にはコードのコメントに書いた通りObjectを継承したままStateTypeを採用できなかったりみたいな細々したところです(もちろん他に見つけ次第また更新します)
しっかり両方のドキュメントを参考にしながら開発をすすめるのがいいと思います。

ライフサイクルについて

データが毎回初期化されてしまう現象が発生していたのでprintデバッグをして調べてみたところ、Store型の変数が更新される度にReducerのhandleActionが実行されてしまうので、handleAction直下にrealmのセーブ処理を書いてしまうと起動時に予期せぬ上書き処理が発生してしまうようです。この時のActionの型はReSwiftInit()型だったのでこの時のみセーブされないように書きなおすことが必要なようです(とりあえずsaveと書きましたが実際はswitch文をもう一個つかってdefaultのほうで保存したりしてます)

最後に

以上で終わりたいと思います。MVVM、Reactive Programmingは今後のトレンドとなりそうですが、このReSwiftもかなり、というかMVVMモデルなんかよりはるかに簡単かつすばらしいモデルだと思うので是非これをきっかけに広まっていってほしいなと思います。是非Realmとあわせて試してみてください!

追記

今回の記事で書いた技術を用いたアプリをリリースしました。

コンタクト専用リマインダーアプリ Eyequette

是非ダウンロードお願いします。
またその他にもいくつかアプリを出しているので気にいるものがあれば是非使ってみてください!

制作アプリ一覧

126
123
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
126
123

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?