Reputation: 2134
I want to be able to access the UndoManager from inside my document model, so I can register undo actions from within the model:
// 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
}
}
To be register the undoManager, I have tried this:
struct DocumentView: View {
let document : MyDocument
@Environment(\.undoManager) var undoManager
var body: some View {
MyDocumentEditor(document: document)
.onAppear {
document.setUndoManager(undoManager: undoManager)
}
}
}
When running my app and loading a saved document this works. But when starting from a new document the UndoManager is nil.
I've tried things like:
@Environment(\.undoManager) var undoManager {
didSet {
self.document.setUndoManager(undoManager: undoManager)
}
}
My objective here is to try and keep as much logic in the model and the views focusing only on UI stuff as much as possible. I wish that ReferenceFileDocument gave a property to access its associated UndoManager as is available with NSDocument.
Upvotes: 3
Views: 2067
Reputation: 76
After trying to figure this out for more than a day, my takeaway is that the UndoManager
in the Environment
is the one attached to the NSWindow
where the view lives. My solution is:
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(&state, action)
undoManager.registerUndo(withTarget: self){target in
target.send(action.inverted())
}
}
//...other methods...
}
Store
can of course be your document class. Now you can pass the UndoManager
found in the environment from any view that sends actions (pay attentions to sheets and alerts though). Or you automate that step away:
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)
}
}
(maybe you'd need to put the Dispatcher
into a StateObject
, I didn't test that part because I'm happy passing the undo manager as a function argument in my small app).
Upvotes: 0
Reputation: 257729
It looks more natural for SwiftUI to use the following approach
var body: some View {
TopLevelView(document: document, undoManager: undoManager)
}
and
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
}
Upvotes: 2
Reputation: 2134
I have found a solution to this - although it doesn't feel right. At the top level of the View I pass the undoManager to a property I hold on the document:
struct ContentView: View {
let document: MyDocument
@Environment(\.undoManager) var undoManager
var body: some View {
document.setUndoManager(undoManager: undoManager)
return TopLevelView(document: document)
}
}
Upvotes: 0