Захват UndoManager из среды SwiftUI

#swiftui

#swiftui

Вопрос:

Я хочу иметь доступ к UndoManager изнутри моей модели документа, чтобы я мог регистрировать действия отмены из модели:

 // Assume I've extended MyDocument to conform to ReferenceFileDocument elsewhere...
final class MyDocument {
    private var undoManager: UndoManager?

    @Published var aNumber = 5 {
        willSet {
            if let undoManager = undoManager {
                let currentValue = self.aNumber
                undoManager.registerUndo(withTarget: self) { target in
                    target.aNumber = currentValue
                }
            }
        }
    }

    func setUndoManager(undoManager: UndoManager?) {
        self.undoManager = undoManager
    }
}
  

Чтобы зарегистрировать UndoManager, я попробовал это:

 struct DocumentView: View {
    let document : MyDocument
    @Environment(.undoManager) var undoManager
    
    var body: some View {
        MyDocumentEditor(document: document)
        .onAppear {
            document.setUndoManager(undoManager: undoManager)
        }
    }
}
  

При запуске моего приложения и загрузке сохраненного документа это работает. Но при запуске из нового документа UndoManager равен нулю.

Я пробовал такие вещи, как:

 @Environment(.undoManager) var undoManager { 
    didSet { 
        self.document.setUndoManager(undoManager: undoManager)
    }
}
  

Моя цель здесь — попытаться сохранить как можно больше логики в модели и представлениях, сосредоточив внимание только на элементах пользовательского интерфейса, насколько это возможно. Я хотел бы, чтобы ReferenceFileDocument предоставил свойство для доступа к связанному с ним UndoManager, которое доступно с NSDocument.

Ответ №1:

Для SwiftUI выглядит более естественным использовать следующий подход

 var body: some View {
    TopLevelView(document: document, undoManager: undoManager)
}
  

и

 struct TopLevelView: View {
    @ObservedObject var document : MyDocument
    var undoManager: UndoManager?

    init(document: MyDocument, undoManager: UndoManager?) {
       self.document = document
       self.undoManager = undoManager

       self.setUndoManager(undoManager: undoManager)
    }

    // ... other code
}
  

Комментарии:

1. Да, так кажется лучше. Однако UndoManager не соответствует ObservableObject, поэтому его определение свойств необходимо опустить @ObservedObject и, следовательно, быть var undoManager: UndoManager?

Ответ №2:

Я нашел решение для этого, хотя оно кажется неправильным. На верхнем уровне представления я передаю UndoManager свойству, которое я храню в документе:

 struct ContentView: View {
    let document: MyDocument
    @Environment(.undoManager) var undoManager

    var body: some View {
        document.setUndoManager(undoManager: undoManager)
        return TopLevelView(document: document)
    }
}
  

Ответ №3:

После попыток разобраться в этом более одного дня, мой вывод заключается в том, что UndoManager в Environment — это тот, который привязан к NSWindow , где находится представление. Мое решение таково:

 protocol Undoable {
   func inverted() -> Self 
}

class Store<State, Action : Undoable> {

   var state : State 
   var reducer : (inout State, Action) -> Void 

   //...init...

   func send(_ action: Action, undoManager: UndoManager) {//passed as an argument
      reducer(amp;state, action)
      undoManager.registerUndo(withTarget: self){target in 
         target.send(action.inverted())
      }
   }

   //...other methods...

}
  

Store конечно, это может быть ваш класс document. Теперь вы можете передавать UndoManager найденные в среде из любого представления, которое отправляет действия (обратите внимание на таблицы и оповещения). Или вы автоматизируете этот шаг:

 class Dispatcher<State, Action : Undoable> : ObservableObject {

   let store : Store<State, Action> 
   let undoManager : UndoManager //see below
   //...init...

   func send(_ action: Action) {
      objectWillChange.send()
      store.send(action, undoManager: undoManager)
   }

}

struct ContentView<State, Action : Undoable> : View {

   @Environment(.undoManager) var undoManager
   let document : Store<State, Action>

   var body : some View {
      ViewHierarchy().environmentObject(Dispatcher(store: document,
                                                   undoManager: undoManager)
   }

}

  

(возможно, вам нужно было бы поместить Dispatcher в StateObject , я не тестировал эту часть, потому что я счастлив передать диспетчер отмены в качестве аргумента функции в моем небольшом приложении).

Комментарии:

1. Как бы это работало, если бы у вас было такое дополнение, как промежуточное программное обеспечение; выполнение какого-либо действия, которое не исходит из события кнопки / пользовательского интерфейса? Из того, что я могу сказать, .UndoManager равен нулю, если первое не является истинным (что очень … раздражает)