छोटे, इन-मेमरी Mac shape editor के लिए value-state snapshots से शुरुआत करें। बार-बार होने वाले, सीमित दायरे के edits के लिए inverse commands का उपयोग करें, जब उनका छोटा payload अतिरिक्त logic को उचित ठहराता हो। दोनों तरीकों में एक स्थिर owner और हर user action के लिए स्पष्ट commit boundary चाहिए।
यह गाइड एक काम पर केंद्रित है: अपने Swift editor में synthetic shapes को खिसकाने, हटाने और उनका क्रम बदलने के लिए भरोसेमंद undo और redo लागू करना। नीचे दिया गया निर्णय ढाँचा Apple के APIs से निकली engineering recommendation है; Apple snapshots और commands में से किसी एक को सार्वभौमिक रूप से चुनने की सलाह नहीं देता।
हर undo operation को क्या याद रखना चाहिए?
Snapshot पिछली संपादन योग्य state, जैसे shapes की क्रमबद्ध array, दर्ज करता है। Undo उस array को बहाल करता है। Inverse command किसी खास edit को पलटने के लिए ज़रूरी operation दर्ज करता है, जैसे किसी shape के center को उसके पिछले coordinates पर सेट करना।
दोनों प्रस्तुतियाँ Foundation के UndoManager के ज़रिए काम करती हैं। यह किसी object या उसके owner पर operations दर्ज करता है; आपके model को मनमाने assignments के लिए अपने आप observe नहीं करता। आपकी mutation methods को उपयुक्त restoration काम register करना होगा। UndoManager
इतिहास की प्रस्तुति चुनने से पहले संपादन योग्य state तय करें। इस editor में shape IDs, coordinates और array का क्रम शामिल हैं। Hover indicators और drag previews को इसके बाहर रखें। साफ़ तौर पर तय करें कि selection इतिहास में शामिल होगी या नहीं: selection बहाल करने से deletion समझना आसान हो सकता है, लेकिन इससे undo वर्तमान selection भी बदलता है।
पूरे editor के लिए एक ही प्रस्तुति अनिवार्य करने के बजाय, हर operation के लिए यह निर्णय तालिका इस्तेमाल करें:
| Operation या स्थिति | सुझाई गई प्रस्तुति | सुरक्षित रखने वाली जानकारी |
|---|---|---|
| छोटा canvas; कई properties साथ बदलती हैं | Snapshot | पिछली संपादन योग्य value पूरी तरह |
| एक shape को खिसकाना | Inverse setter | स्थिर shape ID और पिछला center |
| एक shape हटाना | शुरुआत में snapshot; ज़रूरत हो तो inverse command | Inverse के लिए: हटाया गया shape पूरा और उसकी मूल position |
| कई shapes का क्रम बदलना | Snapshot, जब तक inverse सीधा न हो | पिछला क्रम और प्रभावित properties |
| मापी गई history memory लागत बहुत अधिक हो | लक्षित inverse commands | सटीक restoration के लिए ज़रूरी सब कुछ |
काम का सवाल यह है: क्या आप उन सभी चीज़ों की सूची बना सकते हैं जिन्हें inverse को बहाल करना होगा? अगर shape हटाने से selection और क्रम भी बदलता है, तो सिर्फ़ हटाई गई ID याद रखना पर्याप्त नहीं है। कई संबंधित values को प्रभावित करने वाले operation के लिए snapshot का audit करना अक्सर आसान होता है।
Snapshot वास्तव में स्वतंत्र कब होता है?
Swift structures और arrays में value semantics होती है। इसलिए पूरी तरह value types से बनी shapes की array किसी पहले की state को बाद के mutations से स्वतंत्र रूप में दर्शा सकती है। लेकिन class instances की array कॉपी करने से उन्हीं instances के references बने रहते हैं; किसी instance को बदलने से आपकी कथित snapshot में दिखाई देने वाली चीज़ भी बदल सकती है। उन references को structure में लपेटने से यह समस्या दूर नहीं होती। Value And Reference Types In Swift
छोटे editor के लिए history में सादा value data रखें: identifiers, coordinates, dimensions और दूसरी संपादन योग्य attributes। Views और mutable rendering objects को snapshot से बाहर रखें। अगर live model classes का उपयोग करता है, तो एक अलग value representation बनाएँ जिसमें उसे बहाल करने के लिए ज़रूरी हर attribute हो।
“Snapshot” शब्द से कोई तय memory लागत न मानें। प्रतिनिधि synthetic canvases पर रखे गए history और edit latency को मापें। उदाहरण के तौर पर payload की गणना करें तो, 1,000 shape records रखने वाली 100 entries में कुल 100,000 record occurrences होंगी; एक-एक shape की 100 inverse entries में सिर्फ़ 100 पिछले centers और identifiers होंगे। ये संख्याएँ मापे हुए bytes नहीं हैं और न ही Swift के storage allocation के बारे में कोई दावा हैं।
History की गहराई सोच-समझकर तय करें। Apple के दस्तावेज़ों के अनुसार levelsOfUndo top-level undo groups की संख्या सीमित करता है और इसकी default value शून्य का मतलब है कि groups असीमित हैं। Group limit उपयोगी है, लेकिन यह bytes का budget नहीं तय करता: batch snapshot और एक-coordinate edit में रखे गए data की मात्रा बहुत अलग हो सकती है। levelsOfUndo
Snapshots और inverse commands redo को कैसे सुरक्षित रखते हैं?
Restoration method को model बदलने से पहले उसका उलटा restoration register करना चाहिए। जब undo उस method को चलाता है, तो उसका registration redo उपलब्ध कराता है। Snapshot के लिए, पिछली state बहाल करने से पहले मौजूदा स्थापित state capture करें। Position command के लिए, सहेजा गया center लागू करने से पहले मौजूदा center capture करें।
Apple की archived undo architecture guide इस परस्पर registration के तरीके को साफ़ तौर पर समझाती है: undo के दौरान बुलाया गया setter redo के लिए ज़रूरी value register करता है। उसके उदाहरण Objective-C में हैं; नीचे दिया गया पूरा Swift उदाहरण मौजूदा closure API इस्तेमाल करता है। Registering Undo Operations
सिर्फ़ इसलिए registration बंद न करें कि isUndoing true है। इस डिज़ाइन में ऐसा करने से redo बनाने वाला registration हट जाएगा। Position बहाल करते समय नई relative movement चलाने से भी बचें: पिछला सटीक center सहेजने से इच्छित restoration स्पष्ट रहता है।
Model-स्तर का पूरा उदाहरण
यह स्वतंत्र macOS program snapshot-आधारित deletion को inverse position setter के साथ जोड़ता है। इसमें बिना persistence या UI dependencies के सिर्फ़ synthetic in-memory values और Foundation का उपयोग होता है। इसे EditorUndo.swift नाम से सहेजें और मौजूदा Swift 6 toolchain तथा macOS SDK से compile करें:
swiftc -parse-as-library EditorUndo.swift -o EditorUndo
./EditorUndo
Program जानबूझकर एक अलग manager का मालिक है और event grouping बंद करता है, ताकि इसके assertions किसी चल रहे app event loop पर निर्भर न हों। हर शुरुआती edit को एक स्पष्ट group मिलता है। Undo या redo के दौरान restoration methods अतिरिक्त groups नहीं खोलते। Apple के अनुसार automatic grouping बंद होने पर undo से पहले groups को स्पष्ट रूप से बंद करना ज़रूरी है। groupsByEvent
import Foundation
struct Center: Equatable {
var x: Double
var y: Double
}
struct Item: Equatable {
let id: Int
var center: Center
}
@MainActor
final class Editor {
private(set) var items: [Item]
let history: UndoManager
init(items: [Item], history: UndoManager) {
precondition(Set(items.map(\.id)).count == items.count)
self.items = items
self.history = history
}
func replaceItems(_ next: [Item], name: String) {
precondition(Set(next.map(\.id)).count == next.count)
guard next != items else { return }
let previous = items
history.registerUndo(withTarget: self) { target in
target.replaceItems(previous, name: name)
}
items = next
history.setActionName(name)
}
func setCenter(id: Int, to next: Center) {
guard let index = items.firstIndex(where: { $0.id == id }) else {
preconditionFailure("History refers to a missing item")
}
let previous = items[index].center
guard previous != next else { return }
history.registerUndo(withTarget: self) { target in
target.setCenter(id: id, to: previous)
}
items[index].center = next
history.setActionName("Move Shape")
}
}
@main
struct Demo {
@MainActor
static func main() {
let history = UndoManager()
history.groupsByEvent = false
history.levelsOfUndo = 100
let original = [
Item(id: 1, center: Center(x: 20, y: 30)),
Item(id: 2, center: Center(x: 80, y: 30))
]
let editor = Editor(items: original, history: history)
func edit(_ operation: () -> Void) {
history.beginUndoGrouping()
defer { history.endUndoGrouping() }
operation()
}
edit {
editor.setCenter(id: 1, to: Center(x: 50, y: 40))
}
let moved = editor.items
edit {
editor.replaceItems(
editor.items.filter { $0.id != 2 },
name: "Delete Shape"
)
}
let deleted = editor.items
history.undo()
precondition(editor.items == moved)
history.undo()
precondition(editor.items == original)
history.redo()
precondition(editor.items == moved)
history.redo()
precondition(editor.items == deleted)
print("Undo and redo checks passed")
}
}
अपेक्षित output है Undo and redo checks passed। उदाहरण की जाँच की गई, लेकिन research environment में Swift compiler उपलब्ध न होने के कारण इसे यहाँ चलाया नहीं गया। यह history के मुख्य हिस्से को दिखाता है; SwiftUI interface को अपने views refresh करने के लिए observable model या adapter भी चाहिए।
Closure editor को सीधे capture करने के बजाय अपने target parameter का उपयोग करता है। Apple के दस्तावेज़ों के अनुसार registration target reference type होना चाहिए, manager उसका unowned reference रखता है, और closure parameter का उपयोग करने से captured target के ज़रिए retain cycle नहीं बनता। registerUndo(withTarget:handler:)
Preconditions model की टूटी हुई धारणाओं को चुपचाप undo operation छोड़ने के बजाय उजागर करती हैं। Production में command boundary को registration से पहले invalid edit अस्वीकार करना चाहिए और application स्तर पर उपयुक्त failure दिखानी चाहिए। History replay के दौरान IDs का न मिलना बताता है कि अपेक्षित history sequence के बाहर किसी चीज़ ने model बदल दिया।
Drag को एक undoable action कैसे बनाएँ?
एक drag कई updates पैदा करता है, जबकि पाठक चाहता है कि पूरा हो चुका एक movement एक बार undo हो। Apple की automatic grouping run loop के चक्रों के अनुसार चलती है, इसलिए इसे पूरे gesture की परिभाषा नहीं माना जा सकता। Explicit groups registrations को एक साथ जोड़ सकते हैं, लेकिन उनकी boundaries भी सोच-समझकर सँभालनी पड़ती हैं। groupsByEvent, beginUndoGrouping()
इस editor के लिए preview-and-commit क्रम अपनाएँ:
- Drag शुरू होने पर target ID और gesture का आधार बनाने वाला committed शुरुआती center रखें।
- अस्थायी translation के लिए
@GestureStateऔरupdating(_:body:)इस्तेमाल करें। Committed items बदले बिना shape को उसके baseline और उस translation के योग पर दिखाएँ। - सफल
onEnded(_:)पर अंतिम gesture value से अंतिम center निकालें और model का undoable setter एक बार बुलाएँ। - अगर नया center committed center के बराबर है, तो registration छोड़ दें।
- Cancellation पर preview हटाएँ और committed model को जस का तस रखें।
यह क्रम SwiftUI में दस्तावेज़ित अंतर का पालन करता है: gesture खत्म या cancel होने पर gesture state reset हो जाती है, जबकि onEnded(_:) सिर्फ़ सफलतापूर्वक पूरा होने पर चलता है। इसलिए cancellation के बाद भी ज़रूरी cleanup केवल onEnded(_:) पर निर्भर नहीं होना चाहिए। Adding interactivity with gestures
Preview के दौरान editor की committed state स्थिर रखें। एक सरल नीति यह है कि drag सक्रिय रहने तक canvas के अन्य mutations और canvas undo बंद रखें, फिर पूरा होने या cancel होने पर उन्हें दोबारा चालू करें। अन्यथा, drag commit स्वीकार करने से पहले तय करें कि बीच में हुआ edit सहेजे गए baseline को कैसे अमान्य करेगा।
अगर editor के किसी अन्य हिस्से को लगातार बदलते committed coordinates देखने हों, तो सिर्फ़ preview render करना उपयुक्त नहीं होगा। उस स्थिति में एक स्पष्ट gesture transaction लागू करें: शुरुआती state सहेजें, अलग-अलग history entries बनाए बिना बीच के updates लागू करें, और सफल होने पर एक restoration register करें। Cancellation पर शुरुआती state बहाल करनी होगी। मनमाने callbacks या asynchronous काम के दौरान साझा undo group खुला न छोड़ें।
Model और undo manager का owner कौन होना चाहिए?
हर स्वतंत्र रूप से संपादन योग्य canvas के लिए एक स्थिर editor owner रखें। वही committed items का मालिक हो और history operations का target बने। Model mutations और undo interactions दोनों main actor पर रखें; Apple के मौजूदा दस्तावेज़ UndoManager और उसके closure handler को main-actor isolated बताते हैं। UndoManager
SwiftUI में view का manager @Environment(\.undoManager) से पढ़ें। हर view evaluation के दौरान नया manager बनाने के बजाय उस manager को स्थिर editor या command layer के ज़रिए आगे दें। Environment value optional है: Apple के अनुसार unsupported contexts में यह nil लौटाता है और वहाँ undo registration छोड़ने की अनुमति देता है। Editing फिर भी हो सकती है, लेकिन उस स्थिति में उपलब्ध undo का संकेत नहीं देना चाहिए। EnvironmentValues.undoManager
AppKit canvas के लिए उपयुक्त responder के ज़रिए manager पाएँ। NSResponder.undoManager का base implementation अगले responder से पूछता है। इसलिए, अगर standard menu actions को manager तक पहुँचना है, तो निजी तौर पर बनाए गए manager को canvas के responder context के साथ स्पष्ट रूप से जोड़ना होगा। उसे सिर्फ़ model में रखने से यह connection स्थापित नहीं होता। NSResponder.undoManager
Standalone उदाहरण की grouping configuration उसके अलग manager की है। जब तक साझा manager के हर participant की grouping policy की ज़िम्मेदारी आपकी न हो, उस configuration को environment से मिले manager पर न डालें। UI में context की मौजूदा event policy के अनुसार पूरे हुए canvas edit को register करें; जब कई synchronous suboperations एक action बनाते हों, तो उन्हें group करें।
Editor target को हटाने से पहले, नियंत्रित lifecycle boundary पर उसके लंबित registrations हटाएँ। Apple removeAllActions(withTarget:) देता है, जिससे उस target को संबोधित undo और redo operations हटाए जा सकते हैं। पूरा manager साफ़ करने से साझा manager के अन्य participants भी प्रभावित होंगे। removeAllActions(withTarget:)
अंतिम डिज़ाइन चुनने से पहले क्या जाँचें?
एक छोटा sequence चलाएँ जो दोनों प्रस्तुतियों की सीमाएँ पार करे: एक shape खिसकाएँ, दूसरा हटाएँ, दोनों undo करें, फिर दोनों redo करें। हर चरण के बाद values, IDs और क्रम जाँचें। Cancel किया गया drag, शुरुआती position पर खत्म हुआ drag और दो स्वतंत्र editor windows भी जोड़ें। हर स्थिति अलग सीमा जाँचती है: restoration data, gesture का पूरा होना, no-op संभालना या ownership।
Canvas को बड़े पैमाने पर बदलने को एक स्पष्ट operation मानें। या तो उसे पर्याप्त restoration state के साथ undoable बनाएँ, या पुराने history को जानबूझकर साफ़ करके नया baseline तय करें। संपादन योग्य fields छोड़ देने वाला snapshot और ऐसा inverse command जिसे अपना target न मिले, दोनों अधूरे implementations हैं।
छोटे value-based canvas के लिए snapshots से शुरुआत करें, एक स्थिर owner रखें और हर gesture को एक बार commit करें। जहाँ restoration data स्पष्ट हो और माप से अतिरिक्त logic उचित साबित हो, वहाँ inverse commands जोड़ें। इससे हर user action का अर्थ बदले बिना history की प्रस्तुति बदली जा सकती है।
स्रोत
- UndoManager — Apple
- registerUndo(withTarget:handler:) — Apple
- groupsByEvent — Apple
- beginUndoGrouping() — Apple
- levelsOfUndo — Apple
- EnvironmentValues.undoManager — Apple
- NSResponder.undoManager — Apple
- Adding interactivity with gestures — Apple
- removeAllActions(withTarget:) — Apple
- Registering Undo Operations — Apple, Documentation Archive
- Value And Reference Types In Swift — Swift.org