Created
June 16, 2020 08:43
-
-
Save ollieatkinson/040f66cd0b645bf6d7a79ae5bb9329bc to your computer and use it in GitHub Desktop.
Restorable - Undo/Redo management of values using Swift 5.1 property wrappers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
PublishedRestorable
provides the same undo/redo functionality asRestorable
but also provides a subject to subscribe to changes.Console