अपने Mac ऐप के भीतर खींचे जा रहे आकार की गति को इशारे के दौरान पूर्वावलोकित करें और अंतिम स्थिति को एक छोटे undo समूह में दर्ज करें। मॉडल और undo manager को एक ही मालिक के अधीन रखें, हर समूह-आरंभ के साथ उसका समापन करें, और उलटी गति पंजीकृत करें ताकि redo भी काम करे।
यह ट्यूटोरियल एक छोटा SwiftUI कैनवस बनाता है, जिसमें स्मृति में मौजूद एक कृत्रिम वृत्त है। ठोस उद्देश्य यह है कि पूरी हुई हर ड्रैग के लिए ठीक एक undo लगे, चाहे गति के कितने भी अपडेट आएँ। इसमें यह भी बताया गया है कि AppKit व्यू में वही सीमाएँ कैसे बनाए रखें।
ड्रैग के दौरान undo समूह खुला क्यों नहीं रखना चाहिए
ड्रैग एक ऐसी क्रिया है जो समय के साथ आगे बढ़ती है। Undo समूह उन कार्रवाइयों को इकट्ठा करता है जिन्हें पलटा जा सकता है। इनकी सीमाओं की अवधि एक जैसी होना ज़रूरी नहीं है। Apple के दस्तावेज़ के अनुसार, groupsByEvent का डिफ़ॉल्ट मान true होता है और यह रन लूप के चक्करों के आसपास कार्रवाइयों को समूहित करता है; यह व्यवहार पूरी ड्रैग को एक लेनदेन के रूप में परिभाषित नहीं करता। स्पष्ट रूप से बनाए गए समूह मौजूदा समूहों के भीतर भी रखे जा सकते हैं। Apple के दस्तावेज़ों में स्वचालित इवेंट समूहकरण और undo समूह शुरू करना देखें।
SwiftUI के पहले मूवमेंट कॉलबैक में समूह खोलना और उसे onEnded में बंद करना रद्द किए जाने पर एक खाली जगह छोड़ देता है। Apple स्पष्ट रूप से कहता है कि onEnded केवल तभी चलता है जब इशारा सफलतापूर्वक समाप्त हो। इसके विपरीत, updating के ज़रिए बनाए रखी गई स्थिति इशारा समाप्त होने या रद्द होने पर रीसेट हो जाती है। बेमेल endUndoGrouping() अपवाद उठाता है, इसलिए यह अनुमान लगाना भी ठीक नहीं कि किसी कॉलबैक ने समूह खोला था या नहीं। onEnded(_:), updating(_:body:), और endUndoGrouping() देखें।
इस कैनवस के लिए नीचे दिया डिज़ाइन अपनाएँ। यह दस्तावेज़ में बताई सीमाओं से निकला एक कार्यान्वयन विकल्प है:
| क्रिया का चरण | प्रदर्शन स्थिति | दर्ज मॉडल | Undo समूहकरण |
|---|---|---|---|
| ड्रैग से पहले | सहेजा हुआ केंद्र | मूल केंद्र | बंद |
| मूवमेंट अपडेट | मूल केंद्र और अस्थायी विस्थापन | अपरिवर्तित | बंद |
| सफल समापन | अंतिम केंद्र | एक बार अपडेट | उसी समय खोलें और बंद करें |
| रद्द करना | पूर्वावलोकन रीसेट होता है | अपरिवर्तित | कभी खोला ही नहीं जाता |
| Undo या redo | केंद्र बहाल होता है | पलटने योग्य सेटर के ज़रिए अपडेट | UndoManager द्वारा प्रबंधित |
इस तरीके से वृत्त लगातार हिलता रहता है, लेकिन हर पूर्वावलोकन फ़्रेम को स्थायी बदलाव नहीं माना जाता। पूरी हुई ड्रैग मॉडल का नामित एक ऑपरेशन बन जाती है।
तय करें कि undo इतिहास का मालिक कौन है
इस नमूने में हर कैनवस का अपना मॉडल और निजी undo manager है। चूँकि वही मालिक हर पंजीकरण नियंत्रित करता है, वह आरंभ में एक बार groupsByEvent = false सेट करता है और दर्ज की गई हर गति को स्पष्ट रूप से समूहित करता है। Apple के अनुसार, स्वचालित समूहकरण बंद होने पर undo से पहले समूहों को स्पष्ट रूप से बंद करना ज़रूरी है। नमूना commit विधि से लौटने से पहले समूह बंद करता है। groupsByEvent देखें।
किसी मौजूदा दस्तावेज़ संपादक में पहले से दस्तावेज़ का undo manager हो सकता है। SwiftUI संदर्भगत manager को @Environment(\.undoManager) के माध्यम से उपलब्ध कराता है। Apple के दस्तावेज़ के अनुसार, जब संदर्भ undo का समर्थन नहीं करता, तो इसका मान nil हो सकता है। ऐसी संरचना में दस्तावेज़ का manager और उसकी स्थापित समूहकरण नीति ही इस्तेमाल करें; इस नमूने का निजी-manager विन्यास साझा manager पर लागू न करें। EnvironmentValues.undoManager देखें।
साझा manager में स्वचालित समूहकरण चालू होने पर, समापन के समय एक उलटी गति पंजीकृत करने से भी हर मूवमेंट के लिए अलग इतिहास प्रविष्टि नहीं बनती। स्पष्ट उपसमूह गति को एक साथ रख सकता है, लेकिन इससे वह किसी बाहरी समूह से स्वतंत्र नहीं हो जाता। अगर अन्य बदलाव उसी बाहरी समूह में हैं, तो वे भी साथ में undo हो सकते हैं। एक स्वतंत्र शीर्ष-स्तरीय लेनदेन की गारंटी के लिए साझा इतिहास के मालिक का समन्वय आवश्यक है; चाइल्ड व्यू द्वारा समूह बंद करना पर्याप्त नहीं है।
SwiftUI का पूरा उदाहरण बनाएँ
macOS 14 या उसके बाद के संस्करण को लक्ष्य बनाकर macOS SwiftUI ऐप प्रोजेक्ट बनाएँ। उसकी बनाई हुई ऐप घोषणा और कंटेंट व्यू को नीचे दी गई दोनों फ़ाइलों से बदलें। केवल एक @main घोषणा रखें। स्वामित्व स्पष्ट रखने के लिए नमूने में Undo और Redo के स्थानीय बटन हैं; ऐप्लिकेशन मेनू और responder-chain के साथ एकीकरण अलग चरण है।
मॉडल में पलटने योग्य सेटर है: मौजूदा स्थिति सहेजें, उसे बहाल करने वाला क्लोज़र पंजीकृत करें, फिर नई स्थिति लागू करें। Undo के दौरान वह क्लोज़र चलने पर वही सेटर redo के लिए उलटी कार्रवाई पंजीकृत करता है। Apple Undo Operations पंजीकृत करना में यह पारस्परिक-पंजीकरण पैटर्न दिखाता है। मौजूदा दस्तावेज़ों में UndoManager और उसका पंजीकरण हैंडलर मुख्य अभिनेता तक सीमित भी बताए गए हैं, इसलिए नमूने का मॉडल @MainActor का उपयोग करता है। UndoManager देखें।
पूरा मॉडल ShapeModel.swift में रखें:
import Foundation
import Combine
@MainActor
final class ShapeModel: ObservableObject {
@Published private(set) var center = CGPoint(x: 120, y: 100)
@Published private(set) var canUndo = false
@Published private(set) var canRedo = false
private let history: UndoManager
init() {
let manager = UndoManager()
manager.groupsByEvent = false
history = manager
}
func commitDrag(translation: CGSize) {
let destination = CGPoint(
x: center.x + translation.width,
y: center.y + translation.height
)
guard destination != center else { return }
// This owner expects no caller-owned group.
precondition(history.groupingLevel == 0)
history.beginUndoGrouping()
defer {
history.endUndoGrouping()
refreshAvailability()
}
replaceCenter(with: destination)
}
private func replaceCenter(with destination: CGPoint) {
let previous = center
guard previous != destination else { return }
history.registerUndo(withTarget: self) { target in
target.replaceCenter(with: previous)
}
history.setActionName("Move Shape")
center = destination
}
func undo() {
precondition(history.groupingLevel == 0)
guard history.canUndo else { return }
history.undo()
refreshAvailability()
}
func redo() {
precondition(history.groupingLevel == 0)
guard history.canRedo else { return }
history.redo()
refreshAvailability()
}
private func refreshAvailability() {
canUndo = history.canUndo
canRedo = history.canRedo
}
}
defer जानबूझकर beginUndoGrouping() के उसी समकालिक स्कोप में है। Swift स्कोप समाप्त होते ही defer की गई कार्रवाइयाँ चलाता है, जिसमें जल्दी लौटना या त्रुटि फेंकना भी शामिल है। यह अलग-अलग इशारा कॉलबैक में फैला नहीं रह सकता। यह जोड़ी सामान्य नियंत्रण-प्रवाह निकास पर मॉडल के समूह को संतुलित रखती है। नियंत्रण प्रवाह — Deferred Actions देखें।
Undo क्लोज़र self को कैप्चर करने के बजाय उसके target पैरामीटर पर काम करता है। Apple के दस्तावेज़ के अनुसार, manager लक्ष्य का unowned संदर्भ रखता है और retain cycle से बचने के लिए दिए गए पैरामीटर के इस्तेमाल की सलाह देता है। उपयोग में होने पर कैनवस मॉडल को बनाए रखता है और मॉडल अपने इतिहास का मालिक होता है। registerUndo(withTarget:handler:) देखें।
ऐप और कैनवस को ShapeUndoApp.swift में रखें:
import SwiftUI
@main
@MainActor
struct ShapeUndoApp: App {
var body: some Scene {
WindowGroup {
ShapeCanvas()
}
}
}
@MainActor
struct ShapeCanvas: View {
@StateObject private var model = ShapeModel()
@GestureState private var previewTranslation: CGSize? = nil
private var drag: some Gesture {
DragGesture(
minimumDistance: 1,
coordinateSpace: .named("board")
)
.updating($previewTranslation) { value, state, _ in
state = value.translation
}
.onEnded { value in
model.commitDrag(translation: value.translation)
}
}
var body: some View {
let translation = previewTranslation ?? .zero
let isDragging = previewTranslation != nil
VStack(spacing: 16) {
ZStack(alignment: .topLeading) {
Color.clear
Circle()
.fill(Color.blue)
.frame(width: 44, height: 44)
.position(
x: model.center.x + translation.width,
y: model.center.y + translation.height
)
.gesture(drag)
}
.frame(width: 420, height: 300)
.background(Color.gray.opacity(0.12))
.coordinateSpace(name: "board")
.clipped()
HStack {
Button("Undo") {
guard previewTranslation == nil else { return }
model.undo()
}
.disabled(isDragging || !model.canUndo)
Button("Redo") {
guard previewTranslation == nil else { return }
model.redo()
}
.disabled(isDragging || !model.canRedo)
}
}
.padding(20)
}
}
यह जेस्चर एक नामित कैनवस निर्देशांक-स्थान का उपयोग करता है, ताकि विस्थापन बोर्ड के सापेक्ष मापा जाए, हिलते हुए वृत्त के सापेक्ष नहीं। Apple translation को इशारे की शुरुआत से कुल विस्थापन के रूप में परिभाषित करता है। इसे पूर्वावलोकन में असाइन करें; मूवमेंट के दौरान इसे बार-बार मॉडल में न जोड़ें। DragGesture और translation देखें।
वैकल्पिक gesture state यह भी बताती है कि पूर्वावलोकन सक्रिय है या नहीं। पूर्वावलोकन के दौरान इतिहास के दोनों स्थानीय बटन निष्क्रिय रहते हैं। सफल समापन पर अंतिम जेस्चर मान से दर्ज किया जाने वाला विस्थापन मिलता है। रद्द होने पर gesture state रीसेट हो जाती है और सहेजा हुआ केंद्र अपरिवर्तित रहता है; साफ़ करने के लिए कोई खुला समूह नहीं होता।
इस उदाहरण में जानबूझकर बदलाव करने वाला केवल एक घटक है। ड्रैग के दौरान कोई दूसरा कमांड वृत्त को नहीं बदलता। अगर आप कीबोर्ड से मूवमेंट, हटाना या अन्य संपादन कमांड जोड़ते हैं, तो तय करें कि ड्रैग के दौरान हर कमांड अनुपलब्ध होगी या पहले पूर्वावलोकन रद्द करेगी। सभी कमांड इसी नीति से चलाएँ; केवल इन दो बटनों को निष्क्रिय करने से अतिरिक्त कमांड रास्ते नियंत्रित नहीं होंगे।
एक ड्रैग, एक undo और काम करने वाला redo जाँचें
हिसाब समझने के लिए उदाहरण वाले निर्देशांकों पर गौर करें। (120, 100) से शुरू करके, कुल विस्थापन (10, 5), (25, 12), और (40, 20) से पूर्वावलोकन केंद्र क्रमशः (130, 105), (145, 112), और (160, 120) बनते हैं। समापन तक दर्ज केंद्र (120, 100) ही रहता है, फिर एक बार बदलकर (160, 120) हो जाता है।
Undo पंजीकृत सेटर को (120, 100) के साथ बुलाता है। इसे बहाल करने से पहले सेटर (160, 120) सहेजता है और उस स्थिति को redo के लिए पंजीकृत करता है। Undo कॉलबैक में पंजीकरण को न दबाएँ: यही पारस्परिक पंजीकरण redo को बनाए रखता है। Apple Undo Operations पंजीकृत करना में इस व्यवहार का वर्णन करता है।
पलटने योग्य सेटर के भीतर कोड कोई अतिरिक्त स्पष्ट समूह नहीं बनाता। इतिहास को दोबारा चलाते समय समूहकरण UndoManager नियंत्रित करता है; Apple के दस्तावेज़ के अनुसार redo() दोबारा चलाए गए ऑपरेशनों को undo स्टैक पर एक समूह के रूप में दर्ज करता है। कार्रवाई का नाम setActionName के ज़रिए Move Shape के कृत्रिम लेबल के साथ सेट किया जाता है। redo() और setActionName(_:) देखें।
Mac ऐप में ये व्यवहार जाँचें:
- दिशा कई बार बदलते हुए एक ड्रैग पूरी करें। एक
Undoसे आकार अपनी शुरुआती स्थिति में लौटना चाहिए। Redoचुनें। आकार पूरी हुई ड्रैग की अंतिम स्थिति में लौटना चाहिए।- दो अलग-अलग ड्रैग पूरी करें। दो undo कार्रवाइयों से दोनों पिछली स्थितियाँ उलटे क्रम में बहाल होनी चाहिए।
- पहचानी गई ड्रैग को उसकी शुरुआती स्थिति पर समाप्त करें। कोई-बदलाव सुरक्षा को इतिहास प्रविष्टि नहीं बनानी चाहिए।
- आसपास के ऐप में रद्द किए जाने या व्यू हटाए जाने का रास्ता आज़माएँ। पूर्वावलोकन मिट जाना चाहिए और कोई गति दर्ज नहीं होनी चाहिए।
- कोई गति undo करने के बाद अलग गति पूरी करें। पिछली redo शाखा मिट जानी चाहिए; Apple के दस्तावेज़ के अनुसार, नया undo पंजीकरण redo स्टैक साफ़ कर देता है।
आखिरी व्यवहार Apple की Undo Manager संरचना के अनुरूप है। ऊपर दिए गए निर्देशांक के हिसाब को एक छोटे, नियतात्मक Python गणना से जाँचा गया। Swift स्रोत का निरीक्षण किया गया, लेकिन इसे यहाँ संकलित या चलाया नहीं गया क्योंकि इस वातावरण में Swift कंपाइलर या macOS SDK उपलब्ध नहीं है।
यही सीमाएँ AppKit कैनवस में लागू करें
मौजूदा NSView के लिए AppKit के इवेंट क्रम को बनाए रखें। Apple का मौजूदा संदर्भ mouseDragged(with:) को बायाँ बटन दबाए रखते हुए होने वाले मूवमेंट और mouseUp(with:) को उस बटन के छोड़े जाने के रूप में परिभाषित करता है। उसके संग्रहित इवेंट गाइड में कस्टम व्यू के लिए तीन मेथड का क्रम और निर्देशांक रूपांतरण बताया गया है। mouseDragged(with:), mouseUp(with:), और माउस इवेंट संभालना देखें।
इन्हीं मेथड पर वही डिज़ाइन लागू करें:
mouseDown(with:)में कृत्रिम आकार पर हिट-टेस्ट करें और उसका शुरुआती केंद्र तथा कैनवस निर्देशांकों में बदली गई पॉइंटर लोकेशन सहेजें।mouseDragged(with:)में सहेजी गई पॉइंटर लोकेशन से विस्थापन निकालें और अस्थायी पूर्वावलोकन दोबारा बनाएँ।mouseUp(with:)में अंतिम विस्थापन निकालें, ट्रैकिंग स्थिति साफ़ करें और मॉडल की समकालिक commit विधि एक बार बुलाएँ।- कैनवस के मालिक के रद्दीकरण या teardown पथ में पूर्वावलोकन छोड़ दें और बिना commit किए ट्रैकिंग स्थिति साफ़ करें।
इस क्रिया के लिए एक ही इवेंट मालिक रखें। अगर SwiftUI में AppKit कैनवस एम्बेड किया गया है, तो मूवमेंट और commit चलाने के लिए SwiftUI जेस्चर या AppKit माउस हैंडलर में से एक चुनें। दोनों से एक ही गति पंजीकृत करने पर दोहरी कार्रवाइयाँ बनेंगी। ड्रॉइंग के निर्देशांक एकसमान रखें, जिसमें कोई भी उलटी निर्देशांक प्रणाली या कैनवस ट्रांसफ़ॉर्म शामिल हो।
Apple का संग्रहित गाइड यह भी बताता है कि तीन-मेथड ट्रैकिंग तरीके के दौरान कीबोर्ड कमांड भेजे जा सकते हैं। इसलिए कोई भी मेनू एकीकरण ड्रैग के दौरान वही नीति अपनाए जो स्थानीय बटन अपनाते हैं। दस्तावेज़ का इतिहास उसके दस्तावेज़ मालिक के पास रखें और सुनिश्चित करें कि मेनू कार्रवाइयाँ उसी मालिक के manager को लक्षित करें।
किसी और का समूह बंद किए बिना समूहकरण की गड़बड़ियाँ पहचानें
अगर एक ड्रैग से कई undo चरण बनते हैं, तो देखें कि क्या मॉडल सेटर मूवमेंट अपडेट के दौरान पंजीकरण कर रहे हैं। इन पंजीकरणों को अंतिम commit तक ले जाएँ। अगर कई ड्रैग एक साथ undo होती हैं, तो manager की स्वचालित समूहकरण नीति और मालिक द्वारा बनाए गए किसी बाहरी समूह की जाँच करें। अगर undo के बाद redo उपलब्ध नहीं है, तो देखें कि बहाली वाले रास्ते ने उलटी कार्रवाई पंजीकृत की थी या नहीं।
अगर समूहकरण से अपवाद आता है, तो समापन कॉल की संख्या बदलने से पहले स्वामित्व जाँचें। Apple के दस्तावेज़ के अनुसार, groupingLevel == 0 का मतलब है कि कोई समूह खुला नहीं है, और एक से अधिक समूह खुले रहने पर undo() अपवाद उठाता है। समूह स्तर शून्य होने तक बंद करने वाला लूप किसी दूसरे कॉलर के समूह बंद कर सकता है। समूह स्तर को निदान के लिए उपयोग करें और मालिक के अनुबंध को लागू करें। groupingLevel और undo() देखें।
इस स्मृति-आधारित कैनवस के लिए उपयोगी सीमा दर्ज की गई गति है: पूर्वावलोकन स्वतंत्र रूप से करें, एक बार commit करें, पलटने योग्य बदलाव पंजीकृत करें और लौटने से पहले समूह बंद करें। इससे हर पूरी हुई ड्रैग के लिए एक undo चरण मिलता है और रद्दीकरण, इवेंट डिलीवरी तथा इतिहास का स्वामित्व स्पष्ट रहता है।
स्रोत
UndoManager — Apple
groupsByEvent — Apple
beginUndoGrouping() — Apple
endUndoGrouping() — Apple
onEnded(_:) — Apple
updating(_:body:) — Apple
undoManager — Apple
Undo Operations पंजीकृत करना — Apple
नियंत्रण प्रवाह — The Swift Programming Language स्रोत — Swift.org
DragGesture — Apple
translation — Apple
redo() — Apple
setActionName(_:) — Apple
Undo Manager — Apple
mouseDragged(with:) — Apple
mouseUp(with:) — Apple
माउस इवेंट संभालना — Apple
groupingLevel — Apple
undo() — Apple