استعد الأشكال المعتمة جزئيًا، والمخططات، وتسميات السحب من رد نداء انتهاء جلسة مصدر السحب. لا يعني رد نداء الخروج من الوجهة سوى أن المؤشر غادر ذلك الهدف. لتطبيق Mac، استخدم واجهة جلسة المصدر في SwiftUI عند توفرها، أو اجعل مصدر AppKit يتولى السحب.

يعالج هذا الدليل مهمة واحدة: إعادة شكل اصطناعي إلى مظهره الطبيعي بعد أن ينتهي المستخدم من سحبه، بما في ذلك الحالات التي لا تقبله فيها أي وجهة. يحتفظ المثال بمحتواه في الذاكرة، ويتعامل مع الإفلات المقبول على أنه نسخ، لذا لا تتطلب استعادة الاستجابة حذف المصدر أو نقله.

لماذا تظل استجابة السحب نشطة خارج أهداف الإفلات؟

يعيّن تنفيذ شائع علامة isDragging عند بدء السحب، ويمسحها في performDrop. يربط ذلك التنظيف بحدث لا يمكن التعامل معه إلا من وجهة تقبل الإفلات. وعند الإفلات فوق مساحة فارغة من النافذة، لا تبقى أي وجهة مسؤولة عن إعادة ضبط المصدر.

تعرّف Apple بروتوكول DropDelegate بأنه واجهة لعرض عُدّل لقبول عمليات الإفلات. وتفيد طريقته dropExited(info:) بأن سحبًا تم التحقق منه خرج من ذلك العرض. وقد يستمر السحب إلى هدف آخر، أو يعود إلى الهدف الأول، أو ينتهي في مكان آخر. لذلك، لا يمكن لخروج السحب من الهدف إثبات انتهاء جلسة المصدر. DropDelegate

وينطبق التمييز نفسه على dropDestination(for:action:isTargeted:): تصف isTargeted ما إذا كان المؤشر داخل منطقة الهدف. استخدمها لإطار الهدف أو خلفيته. واجعل رد نداء جلسة المصدر يتحكم في تعتيم المصدر أو تسمية السحب. dropDestination(for:action:isTargeted:)

وينتج عن هذه الحدود الموثقة مبدأ تصميم مفيد: اربط كل حالة مرئية بالحدث المسؤول عنها والقادر على إنهائها على نحو موثوق.

الاستجابة أو النتيجة المسؤول نقطة إعادة الضبط أو الإتمام
تعتيم شكل المصدر مصدر السحب انتهاء جلسة المصدر
إبراز الوجهة وجهة الإفلات خروج المؤشر، مع تنظيف احترازي عند اكتمال المصدر لسحب محلي
العنصر الاصطناعي المقبول الوجهة/النموذج التحقق من صحة الإفلات ونجاحه
الموارد المصدّرة المؤقتة دورة حياة النقل اكتمال النقل، عند الاقتضاء

ما النهج المدعوم الذي ينبغي لتطبيق Mac استخدامه؟

يسرد مرجع Apple الحالي onDragSessionUpdated(_:) على أنه متاح في macOS 26 والإصدارات الأحدث. ويراقب تحديثات أقرب جلسة سحب في التسلسل الهرمي للعروض التابعة له. أرفقه مباشرة خارج مُعدِّل السحب للمصدر لتوضيح نطاقه. استخدم SDK يعرّف هذه الطريقة فعلًا؛ فإتاحتها وقت التشغيل لا تجعل إعلانًا غير موجود في SDK قابلًا للترجمة. onDragSessionUpdated(_:)

هناك اختلاف في التوثيق يجدر التعامل معه ضمن نطاق ضيق: يعرض مثال المُعدِّل مرحلة منتهية مع وسيطي الموقع والعملية، بينما يعرّف مرجع المرحلة المخصص ended(DropOperation). يطابق المثال أدناه .ended من دون استخراج القيم المرتبطة، لأن استعادة المظهر لا تحتاج إليها. راجع تعريف SDK المثبّت لديك قبل إضافة سلوك يعتمد على العملية. DragSession.Phase.ended(_:)

إذا كان هدف النشر أو SDK لديك لا يتيح استخدام رد نداء SwiftUI، فيوفر مصدر سحب AppKit حدًا موثقًا للاكتمال. تذكر Apple صراحةً أن المصدر يتلقى draggingSession(_:endedAt:operation:) عند انتهاء السحب أو إلغائه. ويجب على المصدر المخصص أن يبدأ تلك الجلسة بنفسه. beginDraggingSession(with:event:source:)

إذا كنت تحتاج فقط إلى إعادة تموضع شكل داخل لوحة واحدة، فضع في اعتبارك استخدام DragGesture محلي بدلًا من جلسة سحب وإفلات على مستوى النظام. تُعاد @GestureState إلى حالتها عند خمول إيماءتها، ما يجعلها مناسبة للاستجابة المؤقتة لتلك الإيماءة. أبقِ هذا الخيار منفصلًا عن سحب النظام الذي يوفّر محتوى قابلًا للنقل إلى الوجهات. GestureState

تنفيذ التنظيف الذي يتولاه المصدر في SwiftUI

أنشئ تطبيق SwiftUI لنظام macOS باستخدام SDK يوفّر onDragSessionUpdated(_:)، واضبط هدف النشر على macOS 26 أو إصدار أحدث، واستبدل تعريفَي التطبيق وعرض المحتوى اللذين أُنشئا تلقائيًا بهذا البرنامج الكامل. يوفّر البرنامج سلسلة اصطناعية واحدة هي shape.circle، ومنطقة استقبال واحدة.

يستخدم المصدر draggable(_:) لتوفير محتوى قابل للنقل. ولا تقبل الوجهة إلا سلسلة واحدة مطابقة، وتعيد false لأي مدخل آخر. توثّق Apple أن القيمة المنطقية التي تعيدها الوجهة تشير إلى نجاح الإفلات. draggable(_:)، dropDestination(for:action:isTargeted:)

import SwiftUI

@main
struct DragFeedbackDemoApp: App {
    var body: some Scene {
        WindowGroup {
            DragFeedbackDemo()
        }
    }
}

struct DragFeedbackDemo: View {
    private let payload = "shape.circle"

    @State private var isDragging = false
    @State private var isTargeted = false
    @State private var acceptedCopies = 0

    var body: some View {
        VStack(spacing: 24) {
            Text("Drag the circle into the receiving area")

            Circle()
                .fill(Color.blue)
                .frame(width: 72, height: 72)
                .opacity(isDragging ? 0.4 : 1)
                .draggable(payload)
                .onDragSessionUpdated { session in
                    switch session.phase {
                    case .initial, .active:
                        isDragging = true
                    case .ended, .dataTransferCompleted:
                        restoreFeedback()
                    default:
                        break
                    }
                }

            Text(isDragging ? "Dragging" : "Ready")

            RoundedRectangle(cornerRadius: 12)
                .fill(isTargeted
                      ? Color.blue.opacity(0.2)
                      : Color.gray.opacity(0.12))
                .overlay {
                    Text("Receiving area")
                }
                .frame(width: 280, height: 120)
                .dropDestination(for: String.self) { items, _ in
                    guard items.count == 1,
                          items.first == payload else {
                        return false
                    }
                    acceptedCopies += 1
                    return true
                } isTargeted: { targeted in
                    isTargeted = targeted
                }

            Text("Accepted copies: \(acceptedCopies)")
        }
        .padding(32)
        .frame(minWidth: 380, minHeight: 400)
    }

    private func restoreFeedback() {
        isDragging = false
        isTargeted = false
    }
}

للعلامتين وظيفتان مختلفتان. تغيّر isDragging عتامة المصدر. وتغيّر isTargeted تعبئة منطقة الاستقبال. لذلك، لا يمكن للدخول إلى منطقة الاستقبال أو مغادرتها أن يعيد المصدر إلى حالته الأصلية عن طريق الخطأ بينما يستمر السحب.

يحدد مرجع المرحلة initial بأنها الجلسة التي توشك على البدء، وactive بأنها الحركة، وended بأنها الاكتمال. يستعيد المثال الاستجابة عند ended، ويجعل إعادة الضبط نفسها آمنة عند dataTransferCompleted أيضًا. ولا ينتظر أبدًا اكتمال النقل وحده لاستعادة استجابة سحب غير ناجح. DragSession.Phase

تصف Apple dataTransferCompleted بأنها النقطة التي تكون فيها العناصر المسحوبة قد نُقلت، ويمكن فيها تنظيف العناصر المؤقتة. لا يحتوي هذا المثال على موارد تصدير مؤقتة. وفي تنفيذ أكبر، أبقِ التنظيف المرئي منفصلًا عن تنظيف الموارد كي لا تؤدي استعادة العتامة إلى التخلص من عنصر لا يزال النقل بحاجة إليه. DragSession.Phase.dataTransferCompleted

تظل الدائرة في التسلسل الهرمي طوال الجلسة. وتزيد عملية إفلات مقبولة عدّاد الوجهة؛ أما الإفلات غير المقبول فلا يغيّر المحتوى. وهذا يجعل المثال مفيدًا لتشخيص الاستجابة بمعزل عن دلالات إعادة الترتيب أو النقل. روجع الكود بمقارنته بالتعريفات المسترجعة؛ ولم يُترجم أو يُشغّل لأن البيئة المتاحة هي Linux ولا تتضمن SwiftUI.

كيف يحافظ البديل في AppKit على ملكية الأحداث؟

لاستخدام إصدار أقدم مدعوم من macOS، استبدل تنفيذ مصدر السحب بعرض NSView مستضاف عبر NSViewRepresentable. ينشئ هذا الغلاف العرض الأصلي ويحدّثه، ويربط ردود ندائه بحالة SwiftUI. تتحكم SwiftUI في تخطيط العرض المُمثَّل، لذا استخدم مُعدِّلات التحجيم في SwiftUI بدلًا من تعيين إطار العرض المُمثَّل بنفسك. NSViewRepresentable

نفّذ البديل كسلسلة واحدة يتولاها المصدر:

  1. في mouseDown(with:) لعرض المصدر الأصلي، احتفظ بحدث النقر بالماوس الفعلي الذي وصل إلى ذلك العرض.
  2. في mouseDragged(with:)، طبّق عتبة السحب التي اخترتها، وتحقق من عدم بدء جلسة ثانية.
  3. أنشئ NSDraggingItem من كاتب لوحة لصق في الذاكرة يحمل معرّف العنصر الاصطناعي.
  4. وفّر صورة شكل مُنشأة، واضبط إطار السحب بإحداثيات نظام العرض المصدر.
  5. استدعِ beginDraggingSession(with:event:source:) باستخدام حدث النقر المحتفظ به والكائن الذي يطبّق NSDraggingSource.
  6. أظهر استجابة المصدر من draggingSession(_:willBeginAt:)، واستعدها دائمًا من draggingSession(_:endedAt:operation:).
  7. امسح حدث الإدخال المحتفظ به وبيانات تتبّع الجلسة عند انتهاء التتبّع. وأبقِ المصدر والمالك لرد النداء نشطين حتى اكتمال الجلسة.

تتبع هذه التفاصيل عقد AppKit الصريح: تأخذ طريقة بدء الجلسة حدث النقر بالماوس الذي بدأها، وتستخدم إطارات عناصر السحب نظام إحداثيات العرض، ويتلقى المصدر رسائل دورة الحياة. يقبل NSDraggingItem كائنًا يطابق NSPasteboardWriting؛ وتضبط طريقته الميسّرة ذات الصورة الواحدة إطار السحب ومحتواه. beginDraggingSession(with:event:source:)، init(pasteboardWriter:)، setDraggingFrame(_:contents:)

لهذا التفاعل داخل التطبيق، أعد .copy للسياق NSDraggingContext.withinApplication، ومجموعة عمليات فارغة للسياقات الأخرى. وهذا اختيار صريح يقتصر على إتاحة النسخ محليًا. يسأل AppKit المصدر عن العمليات التي يسمح بها عبر draggingSession(_:sourceOperationMaskFor:). draggingSession(_:sourceOperationMaskFor:)

استعد المظهر لكل نتيجة اكتمال، لا للنسخ الناجح فقط. واجعل أي تغييرات في النموذج مشروطة بالعملية التي نفذها تطبيقك فعلًا. يقع موضع رد نداء الاكتمال بإحداثيات الشاشة؛ فلا تقارنه مباشرةً بالحدود المحلية لهدف SwiftUI. draggingSession(_:endedAt:operation:)

لا يصبح عرض أصلي شفاف موضوع خلف .draggable موجودة في SwiftUI مصدرًا لتلك الجلسة لمجرد مطابقته للبروتوكول NSDraggingSource. فالمصدر هو الكائن المُمرر عند بدء الجلسة. اختر جهة واحدة لتولي البدء والاكتمال، واجعل سطح السحب الأصلي محصورًا في منطقة التفاعل المقصودة للشكل.

لماذا لا نستخدم مراقبة رفع الماوس أو إلغاء تخصيص المزوّد؟

يراقب مراقب رفع الماوس على مستوى التطبيق توزيع الإدخال، وهو حد مختلف عن اكتمال السحب. توثّق Apple أن مراقبي الأحداث المحليين لا يتلقون الأحداث التي تستهلكها حلقات التتبّع المتداخلة. وهذا القيد يجعل المراقب بديلًا غير مناسب لرد نداء الاكتمال الموثق للمصدر. addLocalMonitorForEvents(matching:handler:)

وبالمثل، تفيد deinit في مزوّد العنصر بعمر الكائن. تستدعي Swift طريقة إزالة التخصيص مباشرةً قبل إلغاء تخصيص مثيل؛ ولا يحدد هذا العقد وقت انتهاء السحب. إن استخدام إتلاف المزوّد إشارةً للاكتمال يربط حالة واجهة المستخدم بملكية المراجع بدلًا من دورة حياة السحب. Deinitialization

وتنطوي إضافة DragGesture ثانية إلى جانب سحب النظام على المشكلة المفاهيمية نفسها: فحالته تتبع تلك الإيماءة. استخدم @GestureState عندما تتولى الإيماءة المحلية التفاعل؛ واستخدم رد نداء جلسة مصدر السحب عندما يتولى سحب النظام ذلك. GestureState

تحقق من الإصلاح باستخدام مصفوفة للاكتمال

استخدم الدائرة الاصطناعية للتحقق من كل مسار على حدة. هذه سلوكيات متوقعة للتنفيذ، وليست نتائج اختبارات مُبلّغًا عنها.

التفاعل استجابة المصدر المتوقعة نتيجة الوجهة المتوقعة
إفلات الدائرة داخل منطقة الاستقبال تعود إلى حالتها الطبيعية يزيد العدد مرة واحدة
الإفلات فوق مساحة فارغة من النافذة تعود إلى حالتها الطبيعية يبقى العدد كما هو
مغادرة منطقة الاستقبال مع استمرار السحب تظل معتمة جزئيًا يُمسح إبراز الهدف
إعادة الدخول وإكمال إفلات صالح تعود إلى حالتها الطبيعية عند الاكتمال يزيد العدد مرة واحدة
الإفلات خارج نافذة التطبيق دون وجهة تقبلها تعود إلى حالتها الطبيعية يبقى العدد كما هو
إلغاء سحب نشط تعود إلى حالتها الطبيعية يبقى العدد كما هو
تكرار عدة عمليات سحب غير ناجحة تستعيد كل جلسة حالتها الطبيعية لا تتراكم نسخ مقبولة

إذا ظل المصدر معتمًا جزئيًا، فتحقق أولًا من أن مراقب الاكتمال يغلّف مصدر السحب الفعلي وأن المصدر لا يزال موجودًا. وإذا استمر إبراز الهدف وحده، فافحص معالجة دخول الهدف وخروجه وعمليات التنظيف المشتركة. وإذا تغيّر العدد المقبول عند رفض الإفلات، فافحص التحقق وتغيير النموذج كلًّا على حدة بعيدًا عن استعادة المظهر.

الحل الدائم هو توضيح الجهة المسؤولة عن الاكتمال: يستعيد المصدر الاستجابة الخاصة بالجلسة كلها، وتتولى الوجهة حالة التحويم ومحتواها المقبول، ويتبع تنظيف النقل دورة حياته الموثقة. وعند فصل هذه المسؤوليات، لا يترك الإفلات خارج جميع الأهداف أي حالة مرئية تنتظر رد نداء من وجهة.

المصادر