सिंथेटिक कार्ड को नेटिव macOS ड्रैग के दौरान अखंड दिखाने के लिए, उसके बाहरी कंटेनर को एक ड्रैग स्रोत दें और उसके प्रीव्यू के रूप में पूरा कार्ड रेंडर करें। यदि आपको प्रीव्यू के शुरुआती फ़्रेम और पॉइंटर ऑफ़सेट पर स्पष्ट नियंत्रण चाहिए, तो AppKit का उपयोग करें।

यह गाइड आपके अपने Mac ऐप में एक इन-मेमरी कार्ड को दो पैनलों के बीच खींचने के बारे में है। व्यावहारिक लक्ष्य यह है कि जेस्चर हैंडलिंग और मॉडल अपडेट स्पष्ट स्वामित्व के अधीन रहें, साथ ही कार्ड की आकृतियों, टेक्स्ट, रिक्त स्थान और पहचान को बनाए रखा जाए।

ड्रैग के किस हिस्से को बदलने की ज़रूरत है?

Apple ट्रांसफ़र किए गए डेटा और ड्रैग के दौरान दिखाए जाने वाले दृश्य रूप को अलग रखता है। SwiftUI का draggable(_:preview:) ड्रैग शुरू होने पर आपके प्रीव्यू व्यू की रेंडरिंग बनाता है। दस्तावेज़ों के अनुसार प्रीव्यू स्रोत व्यू के ऊपर केंद्रित होता है। स्रोत व्यू स्वयं ट्रांसफ़र नहीं होता। Apple के draggable(_:preview:) दस्तावेज़ देखें।

आपको वास्तव में किस नियंत्रण की ज़रूरत है, यह पहचानने के लिए इस निर्णय-सहायक का उपयोग करें:

वांछित व्यवहार समर्थित शुरुआती तरीका
कार्ड का लेबल, आकृतियाँ और बैकग्राउंड साथ बनाए रखना एक पूरा कस्टम प्रीव्यू और एक बाहरी ड्रैग स्रोत
उठाने के संक्रमण के दौरान आउटलाइन समायोजित करना contentShape(.dragPreview, shape)
कार्ड का कौन-सा हिस्सा ड्रैग शुरू कर सकता है, यह बदलना contentShape(.interaction, shape)
कार्ड को दूसरे पैनल में रखना पेलोड की पुष्टि करें और गंतव्य पर मॉडल अपडेट करें
शुरुआती ड्रैग इमेज रेक्टैंगल और पॉइंटर के संबंध को स्पष्ट रूप से सेट करना NSDraggingItem का उपयोग करने वाला AppKit-स्वामित्व वाला सेशन
नेटिव ड्रॉप गंतव्यों के बिना कैनवस के भीतर किसी एलिमेंट को लगातार खिसकाना स्थानीय DragGesture इंटरैक्शन

दोनों कंटेंट शेप में अंतर मायने रखता है: Apple के दस्तावेज़ बताते हैं कि .dragPreview प्रीव्यू शेप को प्रभावित करता है, जबकि .interaction हिट टेस्टिंग नियंत्रित करता है। केवल प्रीव्यू आउटलाइन बदलने से ड्रैग शुरू करने वाला क्षेत्र बड़ा नहीं होता। ContentShapeKinds.dragPreview देखें।

स्थानीय DragGesture जेस्चर सीक्वेंस के ज़रिए ड्रैगिंग गति की जानकारी देता है। कैनवस की स्थिति अपडेट करनी हो तो यह तरीका चुनें। इस लेख के पैनल-से-पैनल नेटिव ड्रॉप कार्य के लिए, ड्रैग जेस्चर इंस्टॉल करने हेतु draggable का उपयोग करें। Apple का DragGesture संदर्भ और ड्रैग-स्रोत गाइड देखें।

पूरे सिंथेटिक कार्ड के लिए एक प्रीव्यू बनाएँ

ड्रैग मॉडिफ़ायर उस सबसे छोटे कंटेनर पर रखें जिसमें वे सभी चीज़ें शामिल हों जिन्हें साथ चलना चाहिए। वृत्त, टेक्स्ट की दो पंक्तियों और गोल बैकग्राउंड से बने कार्ड के लिए वह कंटेनर पूरा कार्ड है। उसके चाइल्ड एलिमेंट्स पर अलग-अलग ड्रैग स्रोत लगाने से अलग-अलग इंटरैक्शन बनेंगे।

स्रोत और प्रीव्यू, दोनों के लिए एक ही प्रेज़ेंटेशन व्यू का पुनः उपयोग करें। उस व्यू को स्पष्ट आयाम और अपना बैकग्राउंड दें; आसपास के पैनल से विरासत में मिली स्टाइलिंग पर निर्भर न रहें। यह रेंडर किए गए प्रीव्यू के व्यवहार से निकली डिज़ाइन संबंधी सलाह है: अपने आप में पूर्ण प्रेज़ेंटेशन होने से दोनों रूपों का मेल बनाए रखना आसान होता है।

पूरा SwiftUI उदाहरण

macOS 13 या उसके बाद के डिप्लॉयमेंट टार्गेट वाला macOS SwiftUI ऐप बनाएँ। जनरेट की गई ऐप एंट्री और कंटेंट व्यू को इस एक फ़ाइल से बदलें, और टार्गेट में ठीक एक @main डिक्लेरेशन रखें। यहाँ उपयोग किए गए कस्टम-प्रीव्यू मॉडिफ़ायर और डेस्टिनेशन ओवरलोड, दोनों के लिए macOS 13 से उपलब्धता दस्तावेज़ित है। draggable(_:preview:) और dropDestination(for:action:isTargeted:) देखें।

import Foundation
import SwiftUI

@main
struct IntactDragDemoApp: App {
    var body: some Scene {
        WindowGroup {
            BoardView()
        }
    }
}

private struct CardFace: View {
    var body: some View {
        HStack(spacing: 14) {
            Circle()
                .fill(Color.orange)
                .frame(width: 36, height: 36)

            VStack(alignment: .leading, spacing: 6) {
                Text("Tile A")
                    .font(.headline)
                Text("Circle and label")
                    .font(.caption)
            }
            .foregroundStyle(Color.white)
        }
        .padding(16)
        .frame(width: 220, height: 100, alignment: .leading)
        .background(Color.indigo)
        .clipShape(RoundedRectangle(cornerRadius: 16))
    }
}

private struct BoardView: View {
    @State private var token = UUID().uuidString
    @State private var isOnRight = false

    var body: some View {
        VStack(spacing: 20) {
            Text("Drag the card between panels")
                .font(.headline)

            HStack(spacing: 24) {
                panel(right: false)
                panel(right: true)
            }
        }
        .padding(24)
    }

    private func panel(right: Bool) -> some View {
        VStack(spacing: 16) {
            Text(right ? "Right" : "Left")
                .font(.headline)

            ZStack {
                if isOnRight == right {
                    CardFace()
                        .contentShape(
                            .interaction,
                            RoundedRectangle(cornerRadius: 16)
                        )
                        .contentShape(
                            .dragPreview,
                            RoundedRectangle(cornerRadius: 16)
                        )
                        .draggable(token) {
                            CardFace()
                        }
                } else {
                    Text("Drop here")
                        .foregroundStyle(.secondary)
                }
            }
            .frame(width: 240, height: 120)
        }
        .frame(width: 260, height: 180)
        .background(
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.gray.opacity(0.12))
        )
        .contentShape(Rectangle())
        .dropDestination(for: String.self) { items, _ in
            guard items == [token] else {
                return false
            }
            isOnRight = right
            return true
        }
    }
}

यह उदाहरण केवल सिंथेटिक ग्राफ़िक्स और इन-मेमरी जनरेट किए गए पहचानकर्ता का उपयोग करता है। String पहले से Transferable के अनुरूप है, इसलिए उसे किसी कस्टम ट्रांसफ़र रिप्रेज़ेंटेशन की ज़रूरत नहीं। Apple इस बिल्ट-इन अनुरूपता को व्यू को ड्रैग स्रोत बनाना में दस्तावेज़ित करता है।

अपेक्षित इंटरैक्शन सीधा है: Tile A को दूसरे पैनल में खींचें, छोड़ें, फिर उसे वापस खींचें। पूरा CardFace चलता हुआ प्रीव्यू देता है। गंतव्य ठीक एक मेल खाता टोकन स्वीकार करता है और लोकेशन की एकमात्र वैल्यू बदलता है। true लौटाने से ड्रॉप सफल माना जाता है; false लौटाने से वह अस्वीकार होता है, जैसा कि डेस्टिनेशन ऐक्शन के दस्तावेज़ित अनुबंध में बताया गया है।

यह macOS ऐप टार्गेट के लिए बनाया गया पूरा उदाहरण है। यहाँ इसे कंपाइल या चलाया नहीं गया है, क्योंकि रिसर्च एनवायरनमेंट में Swift टूलचेन या macOS रनटाइम उपलब्ध नहीं है।

दृश्य निरंतरता को मॉडल के स्वामित्व से अलग रखें

उदाहरण में मूल कार्ड तब तक दिखाई देता रहता है जब तक स्वीकार किया गया ड्रॉप isOnRight को नहीं बदलता। पैनलों के बाहर छोड़ने से मॉडल में कोई बदलाव नहीं होता। कार्ड को पहले से भरे पैनल में छोड़ने पर मौजूदा लोकेशन फिर से असाइन होती है। स्थिति बदलने से पहले बेमेल टोकन या एक से ज़्यादा आइटम अस्वीकार कर दिए जाते हैं।

इस व्यवहार से ड्रैग शुरू होते ही कार्ड हटाने और रद्द होने पर उसे फिर से बनाने की ज़रूरत नहीं पड़ती। प्रीव्यू दृश्य निरंतरता बनाए रखता है; लोकेशन बदलने का स्वामित्व गंतव्य के पास रहता है। बड़े बोर्ड के लिए भी यही पैटर्न स्थिर आइटम पहचानकर्ता और एक प्रामाणिक कलेक्शन पर लागू करें।

Swift की वैल्यू सेमांटिक्स यह समझने में मदद करती हैं कि पेलोड पास करने से मूल मॉडल अपने आप क्यों नहीं खिसकता। स्ट्रक्चर और स्ट्रिंग में वैल्यू सेमांटिक्स होती हैं; उन्हें असाइन या पास करने से आपके ऐप के कलेक्शन में कोई आइटम अपने आप दूसरी जगह नहीं जाता। आपके मॉडल अपडेट में उस स्थानांतरण को स्पष्ट करना होगा। स्ट्रक्चर और क्लास देखें।

ऐप के स्तर पर स्थानांतरण और नेटिव ड्रैग ऑपरेशन में भी अंतर रखें। उदाहरण, स्वीकार किए गए ड्रॉप के बाद साझा बोर्ड की एक स्थिति बदलता है; यह नेटिव मूव नेगोशिएशन कॉन्फ़िगर नहीं करता और न ही मूव कर्सर की गारंटी देता है।

macOS 26 और उसके बाद के संस्करणों के लिए, Apple समर्थित ऑपरेशन घोषित करने हेतु DragConfiguration और किए गए ऑपरेशन को देखने के लिए onDragSessionUpdated(_:) दस्तावेज़ित करता है। गंतव्य तय करता है कि वह कौन-सा ऑपरेशन करेगा। यदि आप नेटिव मूव के बाद स्रोत से आइटम हटाते हैं, तो इसे गंतव्य हैंडलिंग के साथ समन्वित करें, ताकि वही साझा आइटम दो बार न हटे। DragConfiguration और onDragSessionUpdated(_:) देखें।

AppKit को ड्रैग सेशन का स्वामित्व कब देना चाहिए?

जब स्वीकृति की शर्त में स्पष्ट रूप से रखी गई ड्रैग इमेज शामिल हो—जैसे पिकअप के समय पॉइंटर को कार्ड के भीतर उसी बिंदु पर बनाए रखना—तब AppKit का उपयोग करें। SwiftUI का दस्तावेज़ित कस्टम-प्रीव्यू मॉडिफ़ायर पेलोड और प्रीव्यू व्यू लेता है और प्रीव्यू को केंद्रित बताता है। AppKit ड्रैगिंग आइटम के फ़्रेम तक सीधे पहुँच देता है।

AppKit इम्प्लीमेंटेशन के लिए यह क्रम अपनाएँ:

  1. स्रोत NSView को वास्तविक माउस-डाउन इवेंट प्राप्त करने दें और ट्रैक करने दें कि इंटरैक्शन ड्रैग बनता है या नहीं।
  2. पूरे सिंथेटिक कार्ड को एक NSImage में रेंडर करें और उसके पेस्टबोर्ड राइटर के साथ एक NSDraggingItem बनाएँ।
  3. setDraggingFrame(_:contents:) से इमेज और उसका शुरुआती रेक्टैंगल सेट करें।
  4. उस रेक्टैंगल को शुरू करने वाले व्यू के कोऑर्डिनेट सिस्टम में व्यक्त करें, फिर शुरू करने वाले माउस-डाउन इवेंट और अपने NSDraggingSource के साथ beginDraggingSession(with:event:source:) कॉल करें।
  5. गंतव्य को ड्रॉप की पुष्टि करने और उसे लागू करने दें। सेशन की सफ़ाई के लिए स्रोत का कम्प्लीशन कॉलबैक उपयोग करें।

Apple बताता है कि शुरू करने वाले इवेंट की माउस लोकेशन ड्रैग किए गए आइकन का ऑफ़सेट तय करती है और हर आइटम के शुरुआती फ़्रेम में स्रोत व्यू के कोऑर्डिनेट इस्तेमाल होने चाहिए। कार्ड का रेक्टैंगल और इवेंट लोकेशन एक ही कोऑर्डिनेट सिस्टम में रखें। beginDraggingSession(with:event:source:) देखें।

setDraggingFrame(_:contents:) एक इमेज से दिखाए गए ड्रैगिंग आइटम के लिए दस्तावेज़ित सुविधाजनक मेथड है। सिंथेटिक कार्ड में कई ड्रॉ किए गए एलिमेंट हो सकते हैं, फिर भी उसे उसी एक इमेज से दिखाया जा सकता है। setDraggingFrame(_:contents:) देखें।

इवेंट और सेशन की सीमाएँ बनाए रखें

कार्ड के इंटरैक्टिव क्षेत्र के लिए एक ही ड्रैग आरंभकर्ता चुनें। इम्प्लीमेंटेशन नियम के तौर पर, उसी क्षेत्र के सक्रिय SwiftUI draggable स्रोत के ऊपर AppKit सेशन शुरू करने वाला कोड न रखें। दोनों इंटरैक्शन का स्वामित्व लेने की कोशिश करेंगे। जेस्चर का स्वामित्व स्पष्ट न होने की भरपाई के लिए माउस इवेंट न बनाएँ।

आप NSViewRepresentable का उपयोग करके AppKit स्रोत एम्बेड कर सकते हैं। नेटिव इवेंट हैंडलिंग को उसके NSView में रखें और स्वीकार किए गए मॉडल बदलावों को बाइंडिंग या कोऑर्डिनेटर के ज़रिए संप्रेषित करें। दर्शाए गए व्यू का लेआउट SwiftUI नियंत्रित करता है; Apple विशेष रूप से कहता है कि उस प्रबंधित व्यू का frame या bounds सीधे सेट न करें। ड्रैगिंग आइटम का इमेज रेक्टैंगल एक अलग विषय है। NSViewRepresentable देखें।

NSDraggingItem को बाद में बदलने के लिए अपने पास न रखें। Apple इसकी सीमित अवधि दस्तावेज़ित करता है: सेशन बनने के बाद, ड्रैगिंग आइटम में बदलाव सेशन एन्यूमरेशन के ज़रिए करें और प्रत्येक एन्यूमरेट किए गए आइटम का उपयोग केवल उसके कॉलबैक के भीतर करें। ड्रैग किसी गंतव्य के ऊपर होने पर गंतव्य की ओर से किए गए बदलाव स्रोत की ओर से किए गए इमेज बदलावों को भी ओवरराइड कर सकते हैं। NSDraggingItem और enumerateDraggingItems(options:for:classes:searchOptions:using:) देखें।

अगर प्रीव्यू अब भी गलत दिखे तो उसका निदान कैसे करें?

समस्या जिस चरण पर दिखाई देती है, उसी पर जाँच करें:

लक्षण पहली जाँच
केवल वृत्त या टेक्स्ट चलता है पूरे कार्ड कंटेनर पर एक ड्रैग स्रोत रखें।
प्रीव्यू के आयाम कार्ड से अलग हैं उसी प्रेज़ेंटेशन व्यू और स्पष्ट आयामों का पुनः उपयोग करें।
पिकअप की आउटलाइन चलती हुई इमेज से अलग है .dragPreview को कस्टम प्रीव्यू क्लोज़र से अलग जाँचें।
खाली जगह से अनपेक्षित रूप से ड्रैग शुरू हो जाता है .interaction और स्रोत कंटेनर की सीमाएँ देखें।
पिकअप पर AppKit इमेज उछलती है शुरू करने वाले माउस-डाउन इवेंट और स्रोत-व्यू के कोऑर्डिनेट जाँचें।
बाद में किए गए AppKit इमेज बदलाव असर नहीं करते उन्हें सेशन एन्यूमरेशन के भीतर करें।
रद्द होने के बाद कार्ड गायब हो जाता है मॉडल म्यूटेशन को स्वीकार किए गए ड्रॉप हैंडलिंग में ले जाएँ और सफ़ाई प्रक्रिया की जाँच करें।

रद्दीकरण, स्वीकार किया गया ड्रॉप, मूल पैनल पर वापस ड्रॉप और अस्वीकृत पेलोड—इन सबकी अलग-अलग पुष्टि करें। उपयोगी इनवेरिएंट यह है कि पूरे समय मॉडल की एक ही लोकेशन प्रामाणिक रहे। पूरे SwiftUI प्रीव्यू से शुरू करें, फिर AppKit-स्वामित्व वाला स्रोत तभी अपनाएँ जब आवश्यक इमेज प्लेसमेंट के लिए उसके स्पष्ट फ़्रेम और इवेंट नियंत्रण की ज़रूरत हो।

स्रोत