├── .gitignore └── UndoRedoImmutableModels.playground ├── Contents.swift ├── contents.xcplayground └── playground.xcworkspace ├── contents.xcworkspacedata └── xcuserdata └── bencz.xcuserdatad └── UserInterfaceState.xcuserstate /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /UndoRedoImmutableModels.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import Cocoa 4 | 5 | // MARK: Core Types 6 | 7 | enum Color { 8 | case red 9 | case blue 10 | case yellow 11 | } 12 | 13 | struct Annotation: Hashable, Equatable { 14 | let id: UUID 15 | var color: Color 16 | 17 | init(id: UUID = UUID(), color: Color) { 18 | self.id = id 19 | self.color = color 20 | } 21 | 22 | var hashValue: Int { 23 | return self.id.hashValue 24 | } 25 | 26 | static func ==(lhs: Annotation, rhs: Annotation) -> Bool { 27 | return lhs.id == rhs.id 28 | } 29 | } 30 | 31 | // MARK: Undo/Redo Types 32 | 33 | struct UndoRedoStep { 34 | let oldValue: T? 35 | let newValue: T? 36 | 37 | /// Converts and undo step into a redo step and vice-versa. 38 | func flip() -> UndoRedoStep { 39 | return UndoRedoStep(oldValue: self.newValue, newValue: self.oldValue) 40 | } 41 | } 42 | 43 | // MARK: Scaffolding 44 | 45 | class DB { 46 | var state: Set = [] 47 | 48 | init() {} 49 | 50 | func saveAnnotation(annotation: Annotation) { 51 | // Replace old with new 52 | self.state.remove(annotation) 53 | self.state.insert(annotation) 54 | } 55 | 56 | func delete(annotation: Annotation) { 57 | self.state.remove(annotation) 58 | } 59 | 60 | func create(annotation: Annotation) { 61 | self.state.insert(annotation) 62 | } 63 | 64 | } 65 | 66 | class AnnotationStore { 67 | 68 | var db: DB 69 | var state: Set = [] 70 | 71 | var undoStack: [UndoRedoStep] = [] 72 | var redoStack: [UndoRedoStep] = [] 73 | 74 | init(db: DB) { 75 | self.db = db 76 | } 77 | 78 | func annotationById(annotationId: UUID) -> Annotation? { 79 | return self.state.first { $0.id == annotationId } 80 | } 81 | 82 | func save(annotation: Annotation, isUndoRedo: Bool = false) { 83 | // Don't record undo step for actions that are performed 84 | // as part of undo/redo. 85 | if !isUndoRedo { 86 | // Fetch old value 87 | let oldValue = self.annotationById(annotationId: annotation.id) 88 | // Store change on undo stack 89 | let undoStep = UndoRedoStep(oldValue: oldValue, newValue: annotation) 90 | self.undoStack.append(undoStep) 91 | 92 | // Reset redo stack after each user action that is not an undo/redo. 93 | self.redoStack = [] 94 | } 95 | 96 | // Update in-memory state. 97 | self.state.remove(annotation) 98 | self.state.insert(annotation) 99 | 100 | // Update db state. 101 | self.db.saveAnnotation(annotation: annotation) 102 | } 103 | 104 | func delete(annotation: Annotation, isUndoRedo: Bool = false) { 105 | if !isUndoRedo { 106 | // Fetch old value 107 | let oldValue = self.annotationById(annotationId: annotation.id) 108 | // Store change on undo stack 109 | let undoStep = UndoRedoStep(oldValue: oldValue, newValue: nil) 110 | self.undoStack.append(undoStep) 111 | 112 | // Reset redo stack after each user action that is not an undo/redo. 113 | self.redoStack = [] 114 | } 115 | 116 | self.state.remove(annotation) 117 | self.db.delete(annotation: annotation) 118 | } 119 | 120 | func undo() { 121 | guard let undoRedoStep = self.undoStack.popLast() else { 122 | return 123 | } 124 | 125 | self.perform(undoRedoStep: undoRedoStep) 126 | 127 | self.redoStack.append(undoRedoStep.flip()) 128 | } 129 | 130 | func redo() { 131 | guard let undoRedoStep = self.redoStack.popLast() else { 132 | return 133 | } 134 | 135 | self.perform(undoRedoStep: undoRedoStep) 136 | 137 | self.undoStack.append(undoRedoStep.flip()) 138 | } 139 | 140 | func perform(undoRedoStep: UndoRedoStep) { 141 | // Switch over the old and new value and call a store method that 142 | // implements the transition between these values. 143 | switch (undoRedoStep.oldValue, undoRedoStep.newValue) { 144 | // Old and new value are non-nil: update. 145 | case let (oldValue?, _?): 146 | self.save(annotation: oldValue, isUndoRedo: true) 147 | // New value is nil, old value was non-nil: create. 148 | case (let oldValue?, nil): 149 | // Our `save` implementation also handles creates, but depending 150 | // on your DB interface these might be separate methods. 151 | self.save(annotation: oldValue, isUndoRedo: true) 152 | // Old value was nil, new value was non-nil: delete. 153 | case (nil, let newValue?): 154 | self.delete(annotation: newValue, isUndoRedo: true) 155 | default: 156 | fatalError("Undo step with neither old nor new value makes no sense") 157 | } 158 | } 159 | 160 | } 161 | 162 | // MARK: Test code 163 | 164 | 165 | let db = DB() 166 | let store = AnnotationStore(db: db) 167 | 168 | var annotation = Annotation(color: .red) 169 | store.save(annotation: annotation) 170 | annotation.color = .blue 171 | store.save(annotation: annotation) 172 | annotation.color = .yellow 173 | store.save(annotation: annotation) 174 | store.delete(annotation: annotation) 175 | 176 | print(store.state) 177 | print(db.state) 178 | 179 | performAndPrint { 180 | store.undo() 181 | } 182 | 183 | performAndPrint { 184 | store.undo() 185 | } 186 | 187 | performAndPrint { 188 | store.undo() 189 | } 190 | 191 | performAndPrint { 192 | store.undo() 193 | } 194 | 195 | performAndPrint { 196 | store.redo() 197 | } 198 | 199 | performAndPrint { 200 | store.redo() 201 | } 202 | 203 | performAndPrint { 204 | store.redo() 205 | } 206 | 207 | performAndPrint { 208 | store.redo() 209 | } 210 | 211 | performAndPrint { 212 | store.undo() 213 | } 214 | 215 | // MARK: Utils 216 | 217 | func performAndPrint(closure: () -> Void) { 218 | closure() 219 | print("Store") 220 | print(store.state) 221 | print("DB") 222 | print(db.state) 223 | } 224 | -------------------------------------------------------------------------------- /UndoRedoImmutableModels.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /UndoRedoImmutableModels.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /UndoRedoImmutableModels.playground/playground.xcworkspace/xcuserdata/bencz.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ben-G/UndoRedoSwift/18a05edb083cfca8ac0c59f9eb75bfe3347a7afe/UndoRedoImmutableModels.playground/playground.xcworkspace/xcuserdata/bencz.xcuserdatad/UserInterfaceState.xcuserstate --------------------------------------------------------------------------------