Hey all! We had a similar need and came up with the following approach. You can set up the state in a similar way to the following:
struct State: Equatable {
var value: String = ""
}
enum Action {
case onDidFinishLaunching // Called when you open your application window for the first time
case closeApp // Called when you close your application
case changeString(String)
}
struct Environment {
var undoClient: TCAUndoClient
}
Where the TCAUndoClient is a struct that provides the following public interface:
struct TCAUndoClient {
var undoEffects: Effect<Action, Never>
func register(_ undoAction: Action, _ actionName: String) -> Void
}
Now, you can implement this struct with something like the following (passing in your UndoManager)
struct TCAUndoClient {
private var undos: PassthroughSubject<Action, Never> = PassthroughSubject()
var undoEffects: Effect<Action, Never> {
return undos.eraseToEffect()
}
init(undoManager: UndoManager) {
self.undoManager = undoManager
}
weak var undoManager: UndoManager?
func register(undoAction: Action, actionName: String) {
undoManager?.registerUndo(withTarget: undos) { undos in
undos.send(undoAction)
}
undoManager?.setActionName(actionName)
}
}
Now, in the environment, we have access to a stream of Actions from the TCAUndoManager, which we can subscribe to when the app launches and unsubscribe from when the app closes. Your reducer would look like this:
let reducer = Reducer<State, Action, Environment> { state, action, environment in
struct UndoEffectsCancellable: Hashable { }
switch action {
case .onDidFinishLaunching:
// Subscribe to the stream of actions
return undoManager.undoEffects
.map { $0 }
.cancellable(id: UndoEffectsCancellable(), cancelInFlight: true)
case .closeApp:
// Unsubscribe
return Effect.cancel(id: UndoEffectsCancellable())
case let .changeString(string, store):
let originalValue = state.string
state.string = string
return .fireAndForget {
environment.register(.changeString(originalValue), "Change String")
}
}
}
And if you wanted to extract the undo logic entirely from your reducer, you could do something like this instead:
let reducer = Reducer<State, Action, Environment> { state, action, environment in
struct UndoEffectsCancellable: Hashable { }
switch action {
case .onDidFinishLaunching:
// Subscribe to the stream of actions
return undoManager.undoEffects
.map { $0 }
.cancellable(id: UndoEffectsCancellable(), cancelInFlight: true)
case .closeApp:
// Unsubscribe
return Effect.cancel(id: UndoEffectsCancellable())
case let .changeString(string, store):
state.string = string
return .none
}
}.undoReducer()
extension Reducer {
func undoReducer() -> Self {
.init { (state, action, env) -> Effect<Action, Never> in
switch action {
case .changeString:
let originalValue = state.string
let effects = self.run(&state, action, env)
environment.register(.changeString(originalValue), "Change String")
return effects
default:
return self.run(&state, action, env)
}
}
}
}