モノトーンの伝説日記

スプラトゥーンが大好き。

UITableView w/Realm で state restoration を実装する。

 state restoration を実装していないアプリなんて存在しないですよね?

 ちなみに Xamarin 環境でのメソッド名にて記述するので,Obj-C や Swift では読み替えてください。

UITableView の state restoration は難しい。

 特に可変の高さを使っている場合など,ちょっと復元方法にテクニックがいるっぽいですね。

 まず,状態保存に関して。

状態保存 (EncodeRestorableState)

  1. TableView.VisibleCells 座標 contentOffset のセル (下記) から ViewModel の Id を取得。その値 (string) を保存。*1
  2. TableView.IndexPathForSelectedRow から ViewModel の Id を取得し,同様に保存。

 座標のセルを取るための補助メソッドは以下の通り。

       [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static UITableViewCell GetCellAtPoint(this UITableView tableView, CGPoint location)
        {
            var indexPathAt = tableView.IndexPathForRowAtPoint(location);
            if (indexPathAt == null) return null;

            return tableView.CellAt(indexPathAt);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static T GetCellAtPoint<T>(this UITableView tableView, CGPoint location)
            where T : UITableViewCell
            => tableView.GetCellAtPoint(location) as T;

var cell = TableView.GetCellAtPoint<YourTableViewCell>(TableView.ContentOffset); でよい。ただし,NavigationBar や TableViewHeader を考慮して取得しないとターゲットセルがかなりずれるので,実際には ContentOffset 直指定は不可。

状態復元 (DecodeRestorableState)

 Realm から読み出す場合,実際のデータベースからの読み込みに多少の時間がかかるため,非同期処理でロードスピナーを表示し,そして復元できる状態になって適切に復元するのが好ましい。

  1. ViewModel として Realm からデータが適切に読み込まれているか,確認。読み込まれていれば復元処理に飛ぶ。
  2. 100% の確率で復元に成功していないので,Rx を使って,ロード処理が終わるまで待機。
  3. ロード処理が終わった場合,データが空でないか確認。空の場合,データベースが消失等が考えられ,この状態では復元できないので,復元しないのが適切。
  4. 以上の条件が全て通過している場合,復元可能。

 ちなみに,復元する前に,TableView.ReloadData() を呼び出さないと,ScrollToRow を使って正しい位置にスクロールされないため,必ず復元前に入れること。

iOS 7 となっていますが,iOS 11.4 現在でも必要です。

stackoverflow.com

まとめ

 具体的なコードは書いていませんが,プロダクトによってかなり復元の仕方が異なると思うので,書いていません。

 面倒臭いですが,state restoration はマルチタスクを利用する際に重要です。ViewModel から Cell に復元する処理は色々あると思いますが,Section - Data の 2 レベルリストを利用しているので,Data.Id を使って実態を検索し,IndexOf を使って NSIndexPath を求めるようにしています。

 もっと復元にいい方法とかあるんですかね。めっちゃ泥臭いコーディングになってるのが個人的に気に食わないですが,とりあえず暫定的にこういうコード書いています……w

*1:スクロール位置で保存してしまうと,バックグラウンドで追加取得した場合など,実際のデータ表示位置とスクロールした位置がずれるためこの方法の方が好ましい