Hello!
In my mixed UIKit/SwiftUI app, I would like to interact with some regular UIKit/AppKit UndoManager, but I'm not sure about the way to setup things.
I guess interacting with the UndoManager is a side effect, so let put it in the environment
struct State: Equatable {
var value: String = ""
}
enum Action {
case changeString(String)
}
struct Environment {
var undoManager: UndoManager
}
The classical way to support undo/redo with the UndoManager with blocks is
func changeString(_ string: String) {
let currentValue = state.value
undoManager.registerUndo(withTarget: self) { target in
target.changeString(currentValue)
}
undoManager.setActionName("change string")
state.value = string
}
In this way, each time it's undoing, it's registering itself for redoing, and reciprocally.
I'm trying to reproduce the same with SCA. I would like to continue using a regular UndoManager because it is automatically linked in several places by UIKit/AppKit.
The issue is that UndoManager needs to act on the behalf of the user and send a .changeString(String)
to some ViewStore
or Store
when it's undoing. UndoManager
doesn't seem to retain the target, so I need to retain it explicitly in case it went away when we are undoing.
So I'm extending UndoManager
:
extension UndoManager {
func registerUndo<State, Action>(action: Action,
store: Store<State, Action>,
name: String? = nil)
where State: Equatable {
registerUndo(withTarget: store) { [store] _ in
ViewStore(store).send(action)
}
name.map(setActionName)
}
}
I'm capturing explicitly the store
(it should be automatic, but I'm marking that's intentional). I'm also creating a single use ViewStore to send the action.
I also need to extend Action
enum Action {
case changeString(String, Store<State, Action>)
}
and the reducer for this action becomes
let reducer = Reducer<State, Action, Environment> {
state, action, environment in
switch action {
case let .changeString(string, store):
let current = state.string
state.string = string
return .fireAndForget {
environment.registerUndo(action: .string1(current, store),
store: store,
name: "change string")
}
}
}
And when I'm making a change, I'm sending .changeString("A", store)
to the ViewStore
.
It seems to work OK. I don't see retain cycles, and captured stores are released after the undo block evaluates. I don't have very much experience with SCA, and maybe I'm doing something completely illegal, or I'm missing something.
Does anybody have any comment or alternative way to achieve this?
Thanks!