Reputation: 379
I need to be able to undo / redo the colors that are picked with the new SwiftUI ColorPicker ( on the iPad : it's presented as a floating window )
The thing that makes it very difficult is that there is apparently no way to know that the user has indeed chosen a color ( and therefore closed the panel )
Instead, the behavior of ColorPicker is that it will keep updating the binded color as the user is manipulating the color controls. This is very helpful to show a live preview, but you don't want to register all these color variations for undo / redo purposes : you only want the color that was finally picked.
Therefore there is no logical distinction between the colors that the user tried, and the one that was selected
And I looked everywhere : there aren't any modifiers / notifications related to that.
I know SwiftUI hasn't been there for long, but this seems like a crucial functionality that's missing?
Has anyone found a workaround?
Upvotes: 7
Views: 1111
Reputation: 2334
Undo is always tricky. One size does not fit all. In this case a good solution is to throttle the number of events that come into the UndoManager
. You can do so by comparing the current time to the time you last commit.
Create a view model that represents your Source of Truth, and conform to ObservableObject
to publish events. It will own the UndoManager. It will have a computed property that gets/sets to the internal data, and check the current time against the time of last commit.
class EditViewModel: ObservableObject {
var commitTime = Date.now
let undoManager = UndoManager()
private var _color: Color = .red {
didSet {
objectWillChange.send()
}
}
var color: Color {
get { _color }
set {
let oldValue = color
let now = Date.now
if now.timeIntervalSince(commitTime) > 1 {
undoManager.registerUndo(withTarget: self) {
$0.color = oldValue
}
}
self.commitTime = now
_color = newValue
}
}
}
Now all that's left to do is create your view and pass in the binding. The implementation details are opaque to the View since it is managed by the ViewModel.
struct EditView: View {
@StateObject var vm = EditViewModel()
var body: some View {
Form {
ColorPicker("Color", selection: $vm.color)
}
}
}
If you need even more control, you can additionally compare the last committed color to the new value and only commit if there is a significant change.
I will also agree that a data-driven approach like SwiftUI's can make undo more tricky. For example, when you need to coalesce multiple operations together into one undo group. By its very nature an undo group is procedural-- the user did one thing after the other, and finally terminates on some condition. But I'm sure there is some way to encapsulate this step-wise operation in a transaction object of some kind.
Undo is hard!
Upvotes: 1