ひとしれずひっそり

主にソフトに関することをメモしていきます。過程をそのまま書いていたりするので間違いが含まれます。鵜呑みしない様に。

UndoManagerの実装

ViewにToggle Swithを置いてOn/Offさせ、その操作のUndo/Redoを試す。

変更を通知できる様にObservableObjectをアダプトする。
objectWillChangeというプロパティが使える様になり、変更前にsend()を送ることでViewに更新が必要な事を伝えることができる。
変更前なのでwillSet{}ブロックで処理する。

この時にUndoManagerに対して、反対の操作をするブロックをregisterUndo()で渡す。
undo()を実行した時にこのブロックが実行されて操作を取り消すことができる。

registerUndo(withTarget:selector:object:)も使える(というか今までこれで試してた)が、@objc属性を付けてObjcのメソッドにしないといけないので、今後はregisterUndo(withTarget:handler:)を使っていこう。

class Model: NSObject, ObservableObject {

    var isOn: Bool = false{
        willSet {
            debugPrint("willSet: \(self.isOn) -> \(newValue)")
              objectWillChange.send()
              let v = self.isOn
              debugPrint("regist handler")
              undoManager.registerUndo(withTarget: self, handler: {
                  debugPrint("handle: \($0.isOn) -> \(v)")
                  $0.isOn = v
              })
        }
        didSet {
            debugPrint("didSet: \(self.isOn)")
        }
    }

SwiftUIでUndoManagerを試してみる · GitHub

これで実行するとUndoがうまく行かない。

シーケンス図に起こしてみると、

Undoで元に戻す処理が実行された後でもう一度呼ばれている。
これによって以降のRedo/Undoがうまく行かなくなる様だ。
これは何処かやり方がまずいからだと思うが、とりあえず最後の呼び出しはUndoManagerに登録しない様にする。
要は値が異なるときだけ処理する様にする。

    var isOn: Bool = false{
        willSet {
            debugPrint("willSet: \(self.isOn) -> \(newValue)")
            if self.isOn != newValue {
                objectWillChange.send()
                let v = self.isOn
                debugPrint("regist handler")
                undoManager.registerUndo(withTarget: self, handler: {
                    debugPrint("handle: \($0.isOn) -> \(v)")
                    $0.isOn = v
                })
            }
        }
        didSet {
            debugPrint("didSet: \(self.isOn)")
        }

これで一応うまく動いている。

ContnetViiewの方も改善した。すっきり。
ModelをNSObjectから継承していたのは既存のソースを改良する場合を想定してなので、最終盤では削除した。

SwiftUIでUndoManagerを試してみる · GitHub

追記:

undo後に余計にsetterが呼ばれるのはViewが書き換えられて、不要になったViewが deallocate される際に呼び出されていのが分かった。
廃棄時は値は変わっていないので比較して同じならUndoManager.registerUndo()を呼び出さないというので合っていると思う。