Skip to content

Instantly share code, notes, and snippets.

@ollieatkinson
Created June 16, 2020 08:43
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ollieatkinson/040f66cd0b645bf6d7a79ae5bb9329bc to your computer and use it in GitHub Desktop.
Save ollieatkinson/040f66cd0b645bf6d7a79ae5bb9329bc to your computer and use it in GitHub Desktop.
Restorable - Undo/Redo management of values using Swift 5.1 property wrappers
@propertyWrapper
public struct Restorable<Value> {
public var wrappedValue: Value
public init(wrappedValue: Value, using undoManager: UndoManager = .init()) {
self.wrappedValue = wrappedValue
self.projectedValue = undoManager
}
public static subscript<Instance>(
_enclosingInstance instance: Instance,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<Instance, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<Instance, Self>
) -> Value where Instance: AnyObject {
get { instance[keyPath: storageKeyPath].wrappedValue }
set {
instance[keyPath: storageKeyPath].projectedValue.registerUndo(of: wrappedKeyPath, on: instance)
instance[keyPath: storageKeyPath].wrappedValue = newValue
}
}
public var projectedValue: UndoManager
}
public struct Edit<Root> where Root: AnyObject {
public var undo: () -> Void
public init<Value>(_ keyPath: ReferenceWritableKeyPath<Root, Value>, on root: Root, currentValue: Value) {
undo = { root[keyPath: keyPath] = currentValue }
}
public static func edit<Value>(_ keyPath: ReferenceWritableKeyPath<Root, Value>, on root: Root, value: Value? = nil) -> Edit<Root> {
.init(keyPath, on: root, currentValue: value ?? root[keyPath: keyPath])
}
}
extension UndoManager {
@inlinable public func registerUndo<Root, Value>(of keyPath: ReferenceWritableKeyPath<Root, Value>, on root: Root, currentValue value: Value? = nil, actionName: String? = nil)
where Root: AnyObject
{
registerUndo(
.edit(keyPath, on: root, value: value),
actionName: actionName
)
}
public func registerUndo<Root>(_ edits: Edit<Root>..., actionName: String? = nil) {
beginUndoGrouping()
if let actionName = actionName {
setActionName(actionName)
}
for handler in edits.map(handler) {
registerUndo(withTarget: self, handler: handler)
}
endUndoGrouping()
}
private func handler<Root>(_ edit: Edit<Root>) -> (UndoManager) -> Void {
return { _ in edit.undo() }
}
/// Removes everything from the undo stack, discards all insertions and deletions, and restores objects to their original values.
public func rollback<Root>(root: Root) {
removeAllActions(withTarget: root)
}
/// Removes everything from the undo stack, discards all insertions and deletions, and restores objects to their original values.
public func rollback() {
removeAllActions()
}
}
#if canImport(Combine)
import Foundation
import Combine
@available(iOS 13.0, macOS 10.15, *)
@propertyWrapper
public struct PublishedRestorable<Value> {
public var wrappedValue: Value {
didSet { wrappedValue$.send(wrappedValue) }
}
private let undoManager: UndoManager
public init(wrappedValue: Value, using undoManager: UndoManager = .init()) {
self.wrappedValue = wrappedValue
self.undoManager = undoManager
}
public static subscript<Instance>(
_enclosingInstance instance: Instance,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<Instance, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<Instance, Self>
) -> Value where Instance: AnyObject {
get { instance[keyPath: storageKeyPath].wrappedValue }
set {
instance[keyPath: storageKeyPath].undoManager.registerUndo(of: wrappedKeyPath, on: instance)
instance[keyPath: storageKeyPath].wrappedValue = newValue
}
}
private let wrappedValue$: PassthroughSubject<Value, Never> = .init()
public lazy var projectedValue: Publisher<Value, Never> = Publisher(wrappedValue$, undoManager)
@dynamicMemberLookup
public struct Publisher<Output, Failure>: Combine.Publisher where Failure : Error {
private var publisher: AnyPublisher<Output, Failure>
private var undoManager: UndoManager
public init<P>(_ publisher: P, _ undoManager: UndoManager) where Output == P.Output, Failure == P.Failure, P: Combine.Publisher {
self.publisher = AnyPublisher(publisher)
self.undoManager = undoManager
}
public func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
publisher.receive(subscriber: subscriber)
}
subscript<Value>(dynamicMember keyPath: KeyPath<UndoManager, Value>) -> Value {
undoManager[keyPath: keyPath]
}
// TODO: Delete this when we have KeyPath to instance members,
// since it can be generically referenced using the dynamicMember subscript.
func undo() { undoManager.undo() }
func redo() { undoManager.redo() }
func rollback() { undoManager.rollback() }
}
}
#endif
@ollieatkinson
Copy link
Author

class Test {
  @Restorable var string = "Hello"
}

test.string = "World!"
test.string // "World!"

test.$string.undo()
test.string // "Hello"

test.$string.redo()
test.string // "World!"

PublishedRestorable provides the same undo/redo functionality as Restorable but also provides a subject to subscribe to changes.

class Test {

  @PublishedRestorable var integer = 0

  func printInteger(integer: Int) {
    print("Value:", integer)
  }

  init() {
    $integer
      .sink(receiveValue: printInteger)
      .store(in: &bag)
  }
  private var bag: Set<AnyCancellable> = []
}

let test = Test()

test.integer += 1 // 1

test.$integer.undo()
test.integer // 0

test.$integer.redo()
test.integer // 1

Console

Value: 1
Value: 0
Value: 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment