[Swift]Value-type なモデルを使った UNDO の実装(その3: ViewModel/View と UNDO の実装)

Model が value-type であることを意識して、ViewModel と View を作り、最終的に UNDO まで作ります。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.2.3
  • Xcode 12.4
  • iOS 14.4

Model

前回モデルとして、struct TODOItem と struct TODOItems を定義しました。どちらも struct で定義し、プロパティも value-type を使っています。

[Swift]Value-type なモデルを使った UNDO の実装(その2: Value-type で モデルを作成)

このモデルの情報を表示するための View と ViewModel を作っていきます。

ViewModel

View を SwiftUI で作成するので、ViewModel は、Observable に準拠させ、Model のデータは、@Published として定義します。

こうすることで、Model のデータ変更で画面の再描画が トリガーされることになります。

# アプリ名を LucidMobile にするつもりなので、クラス名を LucidMobileTODOViewModel にしています。

ViewModel の定義

LucidMobileTODOViewModel

モデルは、外部から渡されることとしました。(CoreData や Realm から読み込みたいときは、読み込んだデータを使って、初期化することとしています。)

また、SwiftUI のビューでリスト表示する時には、要素が配列になっていると便利なので、要素を配列として取り出すメソッドを Model, ViewMode の両方に用意しました。

Model 側のコードは、以下です。

Model(TODOItems) に追加したコード

preview 向けの static 関数定義

SwiftUI で View を定義する時に、以下のような preview のコードがデフォルトで定義されます。

Preview

この preview のコードを使って、Xcode でプレビューできます。プレビュー時にサンプルのデータが入っていると プレビューがわかりやすく、ビューの調整も行いやすくなりますので、以下のようなサンプルデータ用のコードも追加しておきます。

Preview用モデル作成

View (App)

LifeCycle を SwiftUI でつくり、メインのビューの名前を LucidListView とするので、以下のようなコードが、LucidMobileTODOApp のコードとなります。

LucidMobileTODOApp

ViewModel は、@StateObject として定義し、下位のビューには、EnvironmentObject として渡すことにします。

View (LucidListView)

特に value-type な Model であることの特徴はでてこないので、先に View を説明します。

以下は、NavigationBar 左側に EditButton を配置、右側に、+ ボタンを配置して、それぞれ、追加削除できるようにしたものです。
追加時には、シートを表示して詳細を設定できるようにしています。

特に工夫しているところもなく、普通のビューです。

LucidListView

ViewModel (モデル操作と UNDO)

TODOItem の追加削除、UNDO 操作を追加した ViewModel が以下です。

LucidMobileTODOViewModel
コード解説
  1. モデル操作の中心的関数です。(以降で説明します)
  2. View から呼ばれることを想定した TODOItem 追加メソッドです
  3. View から呼ばれることを想定した TODOItem 削除メソッドです
  4. UNDO 周りの関数です

value-type の Model を使った モデル操作と UNDO/REDO の実装説明

WWDC のビデオでも説明していますが、ここが、value-type の Model の見せどころ(?)です。

Model が value-type であることを利用して、操作前の Model をまるっと保存することで UNDO データとします。

複数の箇所で Model 操作を行なってしまうと、不整合が起こるかもしれませんので、ViewModel に changeModel というメソッドを作り集めます。
ポイントは、changeModel の実装です。

changeModel 実装
コード解説
  1. changeModel は、引数に モデル変更操作の closure を受ける
  2. 操作前に、現在のモデルを保存しておく
  3. 変更操作を実行
  4. UndoManager に UNDO を登録
  5. UNDOの内容は、保存しておいた操作前モデルに戻す という操作

上記は抜粋ではなく、changeModel の全てのコードです。value-type な Model の特徴を使うとUNDO は、このようにシンプルな実装になります。

なお、UndoManager は、ViewModel が保持しています。

value-type Model での UNDO のメリット(1)

コードに長所がそのままあらわれています。コードが非常にシンプルです。

実際 UNDO の実装は複雑になりがちです。特に要素間にリンクを入れる等 モデルが複雑化していると 操作前後の状況をきちんと保存して UNDO しなければいけないので非常に複雑になります。

今回の実装方法では、UNDO の操作は非常にシンプルです。「変更前のモデルに戻す」という操作です。
「1つの独立した要素の削除」を行なった時も、「複雑な複数要素間でのリンク操作」を行なった時も同じです。

この点は、強調しても強調しすぎることができないほど良い点です。

value-type Model での UNDO のメリット(2)

実は、もう1つメリットがあります。UNDO の単位を柔軟に設定できること です。

今回の例で言うと、changeModel の clousre の単位が UNDO の単位です。

個々の操作用に UNDO の操作を作り込んでいると、個別操作を組み合わせての UNDO を想定しなければならず、その点も 不具合の温床となりやすい箇所でした。

今回の実装では、どれだけ大きな変更も、1つの closure で渡せば、1つの固まった操作として UNDO できるようになります。

Model を View から見えないように

細かい点ですが、ViewModel は、Model を直接 @Published してしまっていたので、そのままでは、ビューから変更することもできてしまっていました。

private(set) 指定することで、ビューから見えるけれども変更できない状態にすることができます。
こうすることで、変更による描画更新は行われるが、ビューからのモデル変更を防ぐことができます。

# この設定は、Model が value-type であるかどうかに関わらない設定です。

まとめ:value-type なモデルで作る UNDO

value-type なモデルで作る UNDO
  • UNDO は、変更前のモデルを保存しておくだけ
  • モデルを丸ごと保存するだけなので、UNDO 操作をシンプルにできる
  • モデル操作を closure で 呼び出し側で指定できることで、UNDO 単位も柔軟になる

value-type な Model で作ってみた感想

WWDC のビデオで非常に強調されていた点が、自分で手を動かすことで、よくわかります。

これまで、モデルの複雑化に伴った UNDO の複雑化で非常に苦労していたので、ここまで単純になる UNDO には、驚くしかありません。

ただ、copy-on-write の力を信じ切っているわけではないので、モデル丸ごとコピーによるメモリ使用量等をどこかで検証してみようと思います。

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

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