Couples: powered by CoreStore

(I posted an english version of this article here )

Hi guys! I’m John、CouplesのiOSを担当しています。
そして Couplesアプリに使われているCoreStore というSwiftのCore Dataライブラリの作者です。
CouplesIcon
1年前、Swiftが公開された頃からCoreStoreを書き始めて、それからCouplesの重い要件のおかげでライブラリの品質が成長し続けています。

今回Couplesアプリも使用しているCoreStore機能について共有したいと思います。

Couples × CoreStore

型安全

Couplesアプリには多量な画面とコンテンツがあり、うちのCore Dataモデルにはエンティティが40個以上あります: メッセージ、Q&A、カレンダー、リンク、アルバム、写真、記事、スタンプなどがこれに含まれます。
それでもCoreStoreの型安全でgenericsなクラスやメソッドで、キャストゼロでコードを書けます:

extension ArticlesViewController: ListObjectObserver {

    func listMonitor(monitor: ListMonitor<Article>, didUpdateObject object: Article, atIndexPath indexPath: NSIndexPath) {

        let article: Article = monitor[indexPath]
        // ...
    }
}

トランザクション

Couplesアプリでは写真やメッセージなどのデータを非同期で管理しています。
非同期にすることで、UIを固めることなくデータの処理は可能になりますが、いくつかのreadとwriteを並列で実行しているとデータの整合性を失ってしまう危険性があります。
そのため、CoreStoreではreadをメインスレッドから実行して、編集をバックグラウンドスレッドのトランザクションで行っています:

CoreStore.beginAsynchronous { (transaction) -> Void in

    let event: Event = transaction.create(Into(Event))
    event.eventDate = eventDate
    event.startDate = startDate
    event.endDate = endDate
    // ...
    transaction.commit { result in
        // ...
    }
}

内部的に、このトランザクションをシリアルキューで実行しています。
1 SQLiteファイル = 1つトランザクションキュー。
transaction
この設計では処理速度が遅いのではないか?という疑問もあるかとおもいますが、
それは、CoreStoreとデータベース設計で解決できます。

データ分離

CoreStoreは複数のデータスタックを扱える設計になっています。
Couplesでは独立可能なエンティティを別スタックで管理していて、トランザクションは安全に並行で実行できます:
transactions

データインポート

Couplesのサーバーから更新情報がアプリに届くと、CoreStoreは効率的なupdate-insertアルゴリズムでJSONを取り込んでいきます。
これは、 ImportableUniqueObject というprotocolを実装することで簡単に実装が出来ます。

class Article: NSManagedObject, ImportableUniqueObject {
// ...
    func updateFromImportSource(source: JSON, inTransaction transaction: BaseDataTransaction) throws {

        self.linkURL = source["url"] as? String
        self.siteTitle = source["title"] as? String
        self.siteDescription = source["desc"] as? String
        // ...
    }

JSONがサーバーから送られて来たら、トランザクションで簡単にJSON配列をインポートできます

CoreStore.beginAsynchronous { (transaction) -> Void in

    do {

        let articles: [Article] = try transaction.importUniqueObjects(
            Into(Article),
            sourceArray: json["articles"]
        )
    }
    // ...
    transaction.commit { result in
        // ...
    }
}

そしてデータ重複の体制のため、このインポートの仕組みではユニークIDを設定すればユニーク化まで管理してくれます。

リアルタイム更新

Couplesはパートナーから届いたメッセージや写真を素早く表示・更新する必要があります。
この場合、様々なデータを監視するため、NSFetchedResultsControllerやKVOで実装を行いますが、CoreStoreには ListMonitorObjectMonitor というものが用意されており、オブジェクトの監視がシンプルに扱えるようになります。

let anniversaryListMonitor: ListMonitor<Event> = CoreStore.monitorList(
    From(Event),
    Where("isAnniversary == true")
        && Where("startDate >= %@", dateToday)
        && Where("startDate <= %@", dateTomorrow),
    OrderBy(.Ascending("startDate")
)

このクラスではデータ更新を複数のオブザーバーに通知できます。つまり、1つの一覧を複数のview controllerに共通化できます。

Shared.anniversaryListMonitor.addObserver(anniversaryViewController)
// ...
Shared.anniversaryListMonitor.addObserver(calendarViewController)

これはメモリ上もとても効率的です!

マイグレーション

アプリをアップデートするときに、時々Core Dataモデルを更新する必要があって、メンテナンスが大変です。普通のやり方では、過去の各バージョンから、新しいバージョンへのマッピングモデルを作成しなければなりません:
migrationBefore
これを毎回行うのは大変です! CoreStoreでは、インクレメンタルマイグレーションが可能で、メンテナンスは不要になりました!
migrationAfter
つまり、最新のバージョンから新しいバージョンへのマッピングモデルを用意すればいいだけです!

CoreStore.defaultStack = DataStack(
    modelName: "CouplesData",
    migrationChain: ["CouplesDataV1", "CouplesDataV2", "CouplesDataV3", "CouplesDataV4"]
)

おわりに

多量なデータで動いているアプリには(ソーシャルネットワーキングやその他メディアアプリなど)、CoreStoreはおすすめです。
定期的に Githubリポジトリ を更新していますので、質問や依頼などがあれば、是非、IssueとPull Requestをいつでも投稿してください!

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

potatotips#37でプロトコル指向について話してきました

Go Conference 2016 Springで発表してきました