DragGesture, RotateGesture और MagnifyGesture को simultaneously(with:) के साथ संयोजित करें, फिर उनके मानों को एक ही ट्रांसफ़ॉर्म पाइपलाइन के ज़रिए रेंडर करें। Mac कैनवास के लिए, इनपुट सतह को स्थिर रखें और हर जेस्चर के अस्थायी बदलाव को उसके अंतिम रूप से अलग संग्रहीत करें।
यह ट्यूटोरियल आपके अपने macOS ऐप में एक छोटा आकार-दर्शक बनाता है: पैन करने के लिए क्लिक करके खींचें, ज़ूम करने के लिए पिंच करें और आकार घुमाने के लिए ट्रैकपैड रोटेशन जेस्चर का उपयोग करें। इसमें केवल मेमोरी में रखा एक आयत और एक वृत्त है। यह संयोजन पहचाने गए जेस्चर को एक साथ होने देता है; वास्तव में कौन-से इनपुट एक साथ होते हैं, यह डिवाइस द्वारा भेजे गए इवेंट पर निर्भर करता है।
Mac पर simultaneous का क्या अर्थ है?
Apple का simultaneously(with:) दो जेस्चर को मिलाकर एक और जेस्चर बनाता है, इसलिए परिणाम को तीसरे जेस्चर के साथ जोड़ा जा सकता है। इससे पैन, रोटेशन और ज़ूम को संयोजित किया जा सकता है और लंबे प्रेस या किसी अन्य शुरुआती कार्रवाई की आवश्यकता नहीं होती। Apple का simultaneous gesture दस्तावेज़ इस संयोजन का वर्णन करता है।
कोड लिखने से पहले पैन का अर्थ तय करें। यहाँ इसका अर्थ है प्राथमिक माउस बटन दबाकर खींचना, जिसमें ट्रैकपैड पर क्लिक करके खींचना भी शामिल है। ट्रैकपैड पर दो उंगलियाँ सरकाने से आम तौर पर स्क्रॉल इनपुट मिलता है। Apple की संग्रहीत इवेंट गाइड इस स्क्रॉल स्ट्रीम को magnify और rotate जेस्चर से स्पष्ट रूप से अलग बताती है; मौजूदा AppKit दस्तावेज़ में भी स्क्रॉलिंग scrollWheel(with:) के ज़रिए उपलब्ध है। ड्रैग जेस्चर जोड़ने से स्क्रॉलिंग ड्रैगिंग में नहीं बदलती। ट्रैकपैड इवेंट संभालना, scrollWheel(with:)
अपने कैनवास के लिए इनपुट लेयर चुनने हेतु इस निर्णय तालिका का उपयोग करें:
| इच्छित इंटरैक्शन | शुरुआत यहाँ से करें | ध्यान रखने योग्य सीमा |
|---|---|---|
| क्लिक-ड्रैग पैन, पिंच ज़ूम, ट्रैकपैड रोटेशन | नीचे दिया गया SwiftUI उदाहरण | संयोजन पहचानी गई जेस्चर स्ट्रीम को एक साथ होने देता है। |
| पैन करने के लिए दो उंगलियों से स्क्रॉल | SwiftUI में एम्बेड किया गया AppKit कैनवास | स्क्रॉलिंग स्क्रॉल-इवेंट स्ट्रीम ही रहती है। |
| उंगलियों के एक ही पैटर्न से तीनों ट्रांसफ़ॉर्म घटकों की गारंटी | अलग से निर्दिष्ट कस्टम इंटरैक्शन | केवल simultaneous संयोजन यह सुनिश्चित नहीं करता कि हार्डवेयर या macOS हर घटक भेजेगा। |
इस उदाहरण के लिए macOS 14 या उसके बाद का संस्करण आवश्यक है, जहाँ MagnifyGesture और RotateGesture दोनों उपलब्ध हैं। इसमें नए इनपुट-काइंड कॉन्फ़िगरेशन API के बजाय इनके मूल इनिशियलाइज़र का उपयोग किया गया है। MagnifyGesture, RotateGesture
ट्रांसफ़ॉर्म की स्थिति को कैसे व्यवस्थित करें?
हर घटक के लिए एक अंतिम मान और एक लाइव जेस्चर मान रखें। ट्रांसलेशन और रोटेशन जोड़कर संयोजित होते हैं; स्केल गुणा करके। निम्न समीकरण इस उदाहरण का व्यवहार परिभाषित करते हैं; ये SwiftUI की अनिवार्य शर्तें नहीं हैं:
दिखाया गया ट्रांसलेशन = अंतिम ट्रांसलेशन + लाइव ड्रैग ट्रांसलेशन
दिखाया गया रोटेशन = अंतिम रोटेशन + लाइव रोटेशन
दिखाया गया स्केल = clamp(अंतिम स्केल × लाइव मैग्निफ़िकेशन)
लाइव मानों के लिए @GestureState का उपयोग करें। Apple के दस्तावेज़ के अनुसार, जेस्चर निष्क्रिय होने पर यह रीसेट हो जाता है। हर घटक को उसके अपने onEnded कॉलबैक के ज़रिए अंतिम रूप दें, जो केवल सफलतापूर्वक पूरा होने पर चलता है। इससे रद्द होने का अर्थ स्पष्ट रहता है: उस घटक का अधूरा प्रीव्यू हटाएँ और उसका पिछला अंतिम मान बनाए रखें। GestureState, onEnded(_:)
संयोजित करने से पहले ये कॉलबैक अलग-अलग जेस्चर पर रखें। इस तरह हर कॉलबैक को उसका अपना टाइप वाला मान मिलता है और वह केवल उसी गुण को बदलता है जिसका वह स्वामी है। बाद में शुरू होने वाले जेस्चर के लिए कोई साझा शुरुआती ट्रांसफ़ॉर्म स्नैपशॉट नहीं होता जिसे वह अधिलेखित कर सके।
ड्रैग ट्रांसलेशन पर विशेष ध्यान दें: इसका मान जेस्चर शुरू होने के बाद से हुई कुल गति होता है। हर अपडेट पर इस कुल मान को अंतिम ऑफ़सेट में जोड़ने से पहले की गति बार-बार गिनी जाएगी। कुल मान को लाइव प्रीव्यू में रखें, फिर पूरा होने पर इसे एक बार जोड़ें। DragGesture.Value.translation
पूरा SwiftUI उदाहरण बनाएँ
macOS 14 या उसके बाद के डिप्लॉयमेंट टार्गेट के साथ macOS SwiftUI ऐप बनाएँ। जनरेट किए गए ऐप एंट्री और कंटेंट व्यू को नीचे दी गई एकल फ़ाइल से बदलें; टार्गेट में ठीक एक @main घोषणा रखें। किसी एसेट, पैकेज या बाहरी डेटा की ज़रूरत नहीं है।
ग्रे कैनवास जेस्चर का स्वामी है। उसकी चाइल्ड आकृति पर दृश्य ट्रांसफ़ॉर्म लागू होते हैं, इसलिए आकृति को पैन या घुमाने से जेस्चर की इनपुट सतह ट्रांसफ़ॉर्म नहीं होती। उदाहरण जानबूझकर आकृति के केंद्र के आसपास ज़ूम और रोटेट करता है।
import SwiftUI
@main
struct TransformCanvasApp: App {
var body: some Scene {
WindowGroup {
TransformCanvas()
.frame(minWidth: 400, minHeight: 300)
}
.defaultSize(width: 720, height: 500)
}
}
struct TransformCanvas: View {
@State private var offset = CGSize.zero
@State private var angle = Angle.zero
@State private var scale: CGFloat = 1
@GestureState private var liveOffset = CGSize.zero
@GestureState private var liveAngle = Angle.zero
@GestureState private var liveScale: CGFloat = 1
private func boundedScale(_ value: CGFloat) -> CGFloat {
min(max(value, 0.25), 4)
}
private var pan: some Gesture {
DragGesture(minimumDistance: 3, coordinateSpace: .local)
.updating($liveOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
offset = CGSize(
width: offset.width + value.translation.width,
height: offset.height + value.translation.height
)
}
}
private var rotation: some Gesture {
RotateGesture()
.updating($liveAngle) { value, state, _ in
state = value.rotation
}
.onEnded { value in
angle = angle + value.rotation
}
}
private var zoom: some Gesture {
MagnifyGesture()
.updating($liveScale) { value, state, _ in
state = value.magnification
}
.onEnded { value in
scale = boundedScale(scale * value.magnification)
}
}
private var manipulation: some Gesture {
pan.simultaneously(with: rotation)
.simultaneously(with: zoom)
}
var body: some View {
ZStack {
Color.gray.opacity(0.12)
RoundedRectangle(cornerRadius: 18)
.fill(Color.blue)
.overlay(alignment: .topLeading) {
Circle()
.fill(Color.white)
.frame(width: 24, height: 24)
.padding(16)
}
.frame(width: 180, height: 120)
.scaleEffect(
boundedScale(scale * liveScale),
anchor: .center
)
.rotationEffect(angle + liveAngle, anchor: .center)
.offset(CGSize(
width: offset.width + liveOffset.width,
height: offset.height + liveOffset.height
))
.allowsHitTesting(false)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.clipped()
.gesture(manipulation)
}
}
Apple अंतर्निहित ड्रैग, magnify और rotation जेस्चर प्रकारों तथा व्यू पर जेस्चर जोड़ने का दस्तावेज़ देता है। यह नमूना उन API को स्पष्ट स्थिति और निर्देशांक नीति के साथ जोड़ता है। शोध परिवेश में SwiftUI उपलब्ध नहीं था, इसलिए इस प्रोग्राम की दस्तावेज़ों के आधार पर जाँच की गई, लेकिन इसे यहाँ कंपाइल या चलाया नहीं गया। DragGesture, MagnifyGesture, RotateGesture
पहले स्केल, फिर रोटेट, फिर ऑफ़सेट क्यों?
वांछित मैपिंग यह है: आकृति को उसके केंद्र के आसपास स्केल करें, उसी केंद्र के आसपास घुमाएँ, और अंत में कैनवास के निर्देशांकों में ट्रांसलेट करें। ऑफ़सेट को अंत में लागू करने से पैन आकृति के स्केल और रोटेशन से अलग रहता है। आकृति मुड़ जाने के बाद भी पॉइंटर को क्षैतिज दिशा में ले जाने पर आकृति क्षैतिज दिशा में ही चलनी चाहिए।
यह क्रम नमूने की ट्रांसफ़ॉर्म पाइपलाइन पर आधारित डिज़ाइन विकल्प है। Apple दस्तावेज़ करता है कि scaleEffect ऐंकर के आसपास रेंडर किए गए आउटपुट को स्केल करता है, rotationEffect रेंडर किए गए आउटपुट को घुमाता है और offset प्रदर्शित सामग्री को खिसकाता है। ये दृश्य प्रभाव स्थिर कैनवास को ट्रांसफ़ॉर्म किए गए लेआउट कंटेनर में नहीं बदलते। scaleEffect(_:anchor:), rotationEffect(_:anchor:), offset(_:)
0.25 और 4 की सीमाएँ उदाहरण के लिए चुनी गई ऐप सेटिंग हैं। प्रीव्यू और अंतिम मान पर एक ही clamp चलता है, इसलिए जेस्चर छोड़ने पर दिखाई गई स्केल बनी रहती है। Swift के min और max इन सीमाओं को व्यक्त करने के लिए छोटे और बड़े मान चुनते हैं। min(::), max(::)
ठोस अपेक्षाओं के साथ इंटरैक्शन जाँचें
पहले हर घटक को अलग-अलग आज़माएँ, फिर उपलब्ध इनपुट डिवाइस का उपयोग करके उन्हें एक साथ चलाने का प्रयास करें। सफ़ेद वृत्त से रोटेशन दिखाई देता है, भले ही आयत छोटा हो।
- कैनवास पर क्लिक करके खींचें, छोड़ें और फिर से खींचें। दूसरा ड्रैग पहले ड्रैग की अंतिम स्थिति से शुरू होना चाहिए।
- आकृति बड़ी करने के लिए पिंच करें, छोड़ें और फिर से पिंच करें। हर जेस्चर में सुरक्षित रखे गए स्केल से गुणा होना चाहिए।
- घुमाएँ, छोड़ें और फिर से घुमाएँ। कोण बढ़ते जाना चाहिए।
- ज़ूम और रोटेट करने के बाद क्षैतिज दिशा में ड्रैग करें। आकृति को अब भी कैनवास की क्षैतिज धुरी के साथ चलना चाहिए।
- जहाँ डिवाइस एक साथ इनपुट भेजते हैं, वहाँ एक घटक जारी रखते हुए दूसरे को समाप्त करें। जारी घटक को अपना गुण अपडेट करते रहना चाहिए।
- किसी सक्रिय जेस्चर को बाधित करें, उदाहरण के लिए ऐप्लिकेशन बदलकर। सफलतापूर्वक पूरा हुए बिना निष्क्रिय होने वाले किसी भी जेस्चर का लाइव प्रीव्यू हट जाना चाहिए, ताकि अस्थायी स्थिति अटकी न रहे।
गणना की जाँच के लिए मान लें कि सुरक्षित रखा गया स्केल 1.5 है और पिंच 1.2 देता है। दिखाया गया परिणाम 1.8 होगा। यदि सुरक्षित रखा गया कोण 30 डिग्री है और लाइव रोटेशन 15 डिग्री है, तो दिखाया गया कोण 45 डिग्री होगा। 40 पॉइंट का ड्रैग ट्रांसलेशन, उस स्केल और कोण की परवाह किए बिना, 40 पॉइंट का कैनवास ऑफ़सेट रहेगा।
यह नमूना घटकों को स्वतंत्र रूप से अंतिम रूप देता है। यदि रोटेशन सफलतापूर्वक पूरा हो जाता है और बाद में अब भी सक्रिय ड्रैग रद्द हो जाता है, तो पूरा हुआ रोटेशन बना रहेगा और अधूरा ड्रैग वापस हो जाएगा। ऐसे एडिटर को जिसमें सभी घटकों पर एक साथ कार्रवाई लागू होती हो, स्पष्ट commit और rollback नीति वाले अतिरिक्त इंटरैक्शन-सेशन मॉडल की ज़रूरत होगी।
ट्रांसलेशन जानबूझकर असीमित है, इसलिए आकृति दिखाई देने वाले कैनवास से बाहर भी खींची जा सकती है। कैनवास ड्रैग करने योग्य बना रहता है, जिससे आकृति को उलटी दिशा में वापस लाया जा सकता है। तैयार व्यूअर में अलग reset कमांड जोड़ी जा सकती है या सामग्री की अपनी सीमाओं के अनुसार ट्रांसलेशन को सीमित किया जा सकता है।
इसके बजाय AppKit कैनवास कब उपयोग करें?
जब काम के लिए खास तौर पर दो उंगलियों से स्क्रॉल करके पैन करना या स्क्रॉल मोमेंटम पर सीधा नियंत्रण आवश्यक हो, तब AppKit इनपुट लेयर चुनें। NSViewRepresentable के ज़रिए अपना AppKit व्यू एम्बेड करें; Apple इसे SwiftUI के भीतर AppKit व्यू बनाने, अपडेट करने और हटाने के लिए उपलब्ध कराता है। NSViewRepresentable
हर इनपुट स्ट्रीम का एक ही स्वामी रखें। उस AppKit कैनवास में scrollWheel(with:) के ज़रिए स्क्रॉलिंग संभालें और magnification तथा rotation के लिए recognizer का उपयोग करें। मिले बदलावों को उसी ट्रांसलेशन, कोण और स्केल मॉडल तक पहुँचाएँ। उसी मॉडल पर वही इनपुट लागू करने वाला दूसरा SwiftUI हैंडलर न जोड़ें।
साथ-साथ चलने वाले AppKit recognizer के लिए gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) लागू करें और true केवल आपके कैनवास के उन इच्छित recognizer जोड़ों के लिए लौटाएँ। Apple के दस्तावेज़ के अनुसार, delegate method एक साथ recognition की अनुमति देता है; यह अनुपलब्ध इनपुट स्ट्रीम बनाने का तरीका नहीं है। Simultaneous recognition delegate method
ब्रिज के पार मानों के conventions बनाए रखें। AppKit का magnification recognizer एक ऐसी मात्रा देता है जिसे scale factor पाने के लिए 1 में जोड़ना होता है, जबकि उसका rotation recognizer radians देता है। SwiftUI में मानों की असाइनमेंट को बिना बदलाव के कॉपी न करें। NSMagnificationGestureRecognizer.magnification, NSRotationGestureRecognizer.rotation
स्क्रॉलिंग के लिए Apple scrollingDeltaX को पसंदीदा क्षैतिज delta गुण बताता है और यह भी कहता है कि गैर-सटीक delta में समायोजन की ज़रूरत हो सकती है। उसके मोमेंटम दस्तावेज़ में बताया गया है कि शारीरिक जेस्चर रुकने के बाद भी स्क्रॉल इवेंट जारी रह सकते हैं। तय करें कि कैनवास इस जारी स्क्रॉल को स्वीकार करता है या नहीं, और हर स्वीकार किए गए इवेंट को एक ही बार आगे भेजें। scrollingDeltaX, momentumPhase
ब्रिज को आपके कैनवास की सामग्री का ट्रांसफ़ॉर्म बदलना चाहिए, जबकि SwiftUI को प्रतिनिधित्व किए गए व्यू के लेआउट का स्वामी बने रहना चाहिए। Apple प्रतिनिधित्व किए गए व्यू का frame या bounds सीधे सेट करने से स्पष्ट रूप से मना करता है। Delegate और target-action के बदलाव SwiftUI तक पहुँचाने के लिए representable के coordinator का उपयोग करें। NSViewRepresentable
क्लिक-ड्रैग व्यूअर के लिए SwiftUI कार्यान्वयन एक संपूर्ण शुरुआती आधार देता है: एक स्थिर इनपुट सतह, स्वतंत्र रूप से प्रबंधित जेस्चर के तीन घटक और ट्रांसफ़ॉर्म का सोच-समझकर चुना गया क्रम। इसे आगे बढ़ाते समय इन सीमाओं को बनाए रखें और जब उपयोगकर्ता के काम में स्क्रॉल करके पैन करना हो, तो AppKit वाला रास्ता चुनें।
स्रोत
- simultaneously(with:) — Apple
- Handling Trackpad Events — Apple, Documentation Archive
- scrollWheel(with:) — Apple
- MagnifyGesture — Apple
- RotateGesture — Apple
- GestureState — Apple
- onEnded(_:) — Apple
- translation — Apple
- DragGesture — Apple
- scaleEffect(_:anchor:) — Apple
- rotationEffect(_:anchor:) — Apple
- offset(_:) — Apple
- min(::) — Apple
- max(::) — Apple
- NSViewRepresentable — Apple
- gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) — Apple
- magnification — Apple
- rotation — Apple
- scrollingDeltaX — Apple
- momentumPhase — Apple