[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その3 : UNDO/REDO 実装)

Realm を使ったアプリで要素の追加・削除をできるように実装してきました。
今回は、追加・削除にUNDO/REDOを実装してみます。
[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その1 : 要素作成) [SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その2 : 要素編集)

Realm は、UNDO/REDO が苦手

この UNDO/REDO 対応が Realm を使って実装するアプリの課題の1つです。

CoreData であれば、ほぼ全自動で UNDO/REDO 対応のアプリになりますが、Realm では、UNDO/REDO の仕組みから実装しなければなりません。

UNDO/REDO 実装方針

今回は、追加機能と削除機能を UNDO/REDO 対応させようと思います。

追加機能は、追加した要素を記憶しておき、UNDO 時に追加要素を削除することで良いのですが、削除機能の UNDO は簡単ではありません。

削除機能で本当に要素を削除してしまうと UNDO された時に、元に戻せません・・・・

ということで、要素に、”askedForDeletion” フラグを導入して、このフラグが TRUE であれば、削除された要素であると扱うことにします。

注意
この方針以外にも、以下のような実装方針も考えられます。将来的にどのような操作をしていくか等を検討して方針を決める必要があります。

別方針案1:アプリケーションモデルが、処理対象の要素リストを保持し、そのリストから外すことで削除とする。
別方針案2:削除時に実際に削除してしまう。ただし、再作成できる情報をすべて保存し、UNDO 時には、同じ情報を持つように要素を再作成する。

いくつか方針は考えられますが、今回は、フラグをセットするようにして進めることにします。

コマンドパターン

GoF のデザインパターンの1つに “Command Pattern” というものがあります。

Wikipedia の説明は、こちら

今回はこのパターンを採用して実装していきます。

Command が準拠すべき Protocol は、以下のように定義しました。

Command Protocol

1: コマンドの操作対象のモデルを持つようにします。
2: 実行するための関数名を定義します。
3: UNDO/REDO のために、自コマンドの逆操作となるようなコマンドを返します。

作成コマンド、削除コマンド のどちらも、上記 Protocol に準拠して、実装していきます。

AppModel 修正

Command Protocol に準拠するコマンドを実行できるようにするのと、UNDO, REDO 用に、Command のリストを保持しておくようにします。

Command のリストを利用して、アプリケーションとして、UNDO 可能か?、REDO 可能か? の情報を提供できるようにします。

UNDO/REDO 対応 AppModel

1,2: UNDO/REDO 用のコマンドスタック
3,4: UNDO/REDO 可能フラグ
5: コマンド実行関数。実行後、UNDO 用のコマンドを UNDO スタックに記録します。同時に、REDO スタックを空にします。
6, 7: UNDO, REDO 実行関数

コマンド作成

AppModel 側の準備はできたので、作成・削除のコマンドを作っていきます。

気をつけるべき点は、Realm のオブジェクトは、スレッドを超えて渡すことができないので、別スレッドからオブジェクトを操作する時には、primaryKey を使って、Realm から要素を改めて取得する必要があります。

CreateTextLineCommand

以下のようになります。

CreateTextLineCommand

気をつける点は、flippedCommand(UNDO用のコマンド)は、削除コマンドになるという点です。

DeleteTextLineCommand

以下のようになります。

DeleteTextLineCommand

気をつける点は、DeleteTextLineCommand の flippedCommand は、DeleteTextLineCommand であり、CreateTextLineCommand ではありません。

方針のところでも説明しましたが、削除という行為は、削除フラグをセットしているだけなので、そのフラグ操作を行うためです。

注意点

実装してきたように、削除コマンドは実際に削除せずに削除フラグを設定するだけです。

ですので、アプリケーション終了時のようなタイミングで、削除フラグを設定された要素を削除することが必要です。

削除しなくとも UI 的には動きますが、データ量が増え続けてしまいます。

iPhone アプリには、アプリケーション終了の明確なタイミングがありません。別アプリに切り替えた時には、バックグラウンドに遷移した状態になっています。

この辺りは、UNDO/REDO というよりもアプリケーション設計の課題だと思いますので、検討項目のまま残しておくことにします。

考えていくとすると、以下の項目から検討していくことになるかと思います。

  • ユーザーが、UNDO/REDO できなくなっても良いと思えるタイミングは何か?
  • そのタイミングを、iOS で検知することができるか?

これが、Xcode12 から導入されるドキュメントを対象とするアプリであれば、セーブ等のタイミングがそのタイミングになるかと思います。(詳細未検討です)

UNDO/REDO UI

UNDO/REDO の UI についての Apple のドキュメントは、こちら

ざっくりいうと、「UNDO/REDO の対象をきちんと説明しなさい。」「シェイクジェスチャーを使うなら、UNDO/REDO 以外に使ってはいけない」「念の為、UNDO/REDOボタンも用意して」「現在のContextに対してのみUNDO/REDOしなさい」と言ってます。

今回は、UNDO/REDO のためのボタンをつけたいと思います。

すこしタイトルバー周りがごちゃついてしまいましたが、前回のUIから少し変更して以下のようにしました。

UI for UNDO/REDO

コードとしては、以下のようになります。

UNDO/REDO を追加したUI

0: 削除フラグが設定されている要素をフィルターして処理しています。(他の箇所も同様です)
1: UNDO ボタンを SFSymbolを使って表示しています。クリックされた時には、AppModel から UNDO を実行します。
2: UNDO 可能かどうかは AppModel の判定をベースに判断します。
3, 4: REDO ボタンも同様です。
5: EditButton は、右側に移動しました。
6: 追加機能のボタンです。Closure の中でコマンドを作成して、AppModel に実行させています。
7, 8: 削除機能も同様に、コマンドを作成し、AppModel にて実行します。

まとめ:Realm を使った UNDO/REDO は、自分で実装が必要

今回は、Command Pattern を Realm と組み合わせて UNDO/REDO 機能を作りました。

デザインパターンは、いつまでも地味に役立つものだと改めて感じました。

説明は以上です。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です