PointOfNilReturn
PointOfNilReturn

Reputation: 476

UndoManager's canUndo property not updating in SwiftUI

Why does the @Environment UndoManager not update its canUndo property when it has actions in its stack? I have a view that has a child that can utilize the un/redo functionality, but for some reason I can't disable the undo button based on the manager.

struct MyView: View {
    @Environment(\.undoManager) var undoManager: UndoManager?

    var body: some View {
        Button("Undo") { ... }
            .disabled(!self.undoManager!.canUndo)
    }
}

Upvotes: 7

Views: 1952

Answers (2)

Asperi
Asperi

Reputation: 257729

UndoManager.canUndo is not KVO compliant, so use some notification publisher to track state, like below

struct MyView: View {
    @Environment(\.undoManager) var undoManager
    @State private var canUndo = false

    // consider also other similar notifications
    private let undoObserver = NotificationCenter.default.publisher(for: .NSUndoManagerDidCloseUndoGroup)

    var body: some View {
        Button("Undo") { }
            .disabled(!canUndo)
            .onReceive(undoObserver) { _ in
                self.canUndo = self.undoManager!.canUndo
            }
    }
}

Upvotes: 14

Maciek Czarnik
Maciek Czarnik

Reputation: 6181

When it comes to canRedo I tried multiple things, and what I ended up with is this - so observing viewModel (or document or any other undo-supporting data source) and updating canUndo/canRedo in reaction to it's change:

struct MyView: View {
    @ObservedObject var viewModel: ViewModel

    @Environment(\.undoManager) private var undoManger: UndoManager!
    @State private var canUndo = false
    @State private var canRedo = false
    
    var body: some View {
        RootView()
            .onReceive(viewModel.objectWillChange) { _ in
                canUndo = undoManger.canUndo
                canRedo = undoManger.canRedo
            }

        if canUndo {
            Button(
                action: { undoManger?.undo() },
                label: { Text("Undo") }
            )
        }
        if canRedo {
            Button(
                action: { undoManger?.redo() },
                label: { Text("Redo") }
            )
        }
        ...

I also wrapped it in a standalone button (without overgeneralizing the implementation above my own needs) that eliminates the boilerplate from my view and keeps complexity more private so it ends up like this for me:

struct MyView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
       RootView()
       UndoManagerActionButton(
           .undo,
           willChangePublisher: viewModel.objectWillChange
       )
       UndoManagerActionButton(
           .redo,
           willChangePublisher: viewModel.objectWillChange
       )
       ...

Upvotes: 1

Related Questions