لنسخ الأشكال الاصطناعية بين لوحين في تطبيق Mac، ابدأ بالسحب والإفلات ذي الأنواع المحددة في SwiftUI. اختر موفّري العناصر عندما تتطلبهم واجهة موفّر موجودة أو إصدار أقدم مستهدف من macOS. حدّد الحمولة وقواعد القبول ومالك النموذج قبل اختيار أي من واجهتي API.

حدّد معنى نجاح الإفلات

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

توصي Apple بالعناصر القابلة للنقل للسحب والإفلات في Swift. كما تدعم وثائقها المعدّلات القائمة على الموفّرات، لذا يعتمد الاختيار على متطلبات النشر والواجهة التي تحتاج إلى دمجها. السحب والإفلات — Apple.

اكتب عقد النقل قبل تنفيذ الواجهات:

سؤال العقد القرار لهذا المثال
ما الذي يعبر الحد؟ قيمة ذات إصدار تصف نوع الشكل
من يمكنه استلام التمثيل؟ التطبيق المصدر فقط
ماذا يفعل الإفلات الناجح؟ إضافة عنصر جديد ذي معرّف إلى المجموعة
من يملك التعديل؟ حالة المجموعة في واجهة الوجهة
ماذا يحدث عند الإلغاء أو الرفض؟ لا تغيير في النموذج
ما دلالة موضع الإفلات؟ لا شيء هنا؛ تُضاف العناصر المقبولة بالترتيب

هذه قرارات تصميم للتطبيق. تُظهر معاينة السحب الحمولة، بينما تقرر الوجهة كيف تغيّر تلك الحمولة النموذج. ويسمح الفصل بين هاتين المسؤوليتين بتغيير وسيلة النقل دون إعادة كتابة قواعد المجموعة.

اختر أصغر واجهة API تعبّر عن العقد

الحالة نقطة البداية السبب
مصدر ووجهة SwiftUI، وmacOS 13 أو أحدث Transferable و.draggable و.dropDestination محددة النوع التعبير مباشرة عن نوع النموذج المقبول
تكامل قائم مع NSItemProvider .onDrag و.onDrop الإبقاء على تسجيل التمثيل وتحميله صراحةً
إصدار مستهدف أقدم من macOS 13 تحقّق من إتاحة معدّلات الموفّر لهذا الإصدار يتطلب الأساس المحدد النوع macOS 13؛ أما التحميل الزائد للموفّر المستخدم أدناه في النقاش فيتطلب macOS 11
تتولى واجهة AppKit موجودة تفاعل الفأرة واجهات السحب في AppKit الحفاظ على معالجة أحداث الواجهة ودورة حياة جلسة السحب

توثّق Apple Transferable ووجهة الإفلات الأصلية محددة النوع بدءًا من macOS 13. أما التحميل الزائد لـonDrop(of:isTargeted:perform:) القائم على الموفّر، والذي يقبل مصفوفة من قيم UTType، فيتوفر بدءًا من macOS 11. تحقّق من الإصدار الأدنى لكل تحميل زائد بعينه، بدلًا من افتراض أن جميع واجهات السحب والإفلات تشترك في إصدار أدنى واحد. Transferable، dropDestination(for:action:isTargeted:)، onDrop(of:isTargeted:perform:).

إتاحة النقل داخل التطبيق ممكنة مع النهجين. للتمثيل المحدد النوع، استخدم .visibility(.ownProcess)؛ إذ تعرّف Apple مستوى الرؤية هذا بأنه يقتصر على التطبيق المصدر. لذا لا داعي لاختيار موفّر عناصر لمجرد إبقاء النقل داخل التطبيق. visibility(_:)، ownProcess.

أنشئ تفاعل النسخ المحدد النوع

يتكون هذا المثال الكامل من ملفي Swift في هدف جديد لتطبيق macOS يستخدم SwiftUI. عيّن هدف النشر إلى macOS 13 أو أحدث، واستخدم وضع لغة Swift 5 لهذا الأساس. استبدل نقطة الدخول التي أنشأها المشروع بالملف الثاني؛ وأبقِ تصريح @main واحدًا فقط. راجعت الشيفرة، لكنني لم أتمكن من تجميعها هنا لأن الصدفة المتاحة لا تتضمن سلسلة أدوات Swift.

عرّف الحمولة ونوع المحتوى الخاص بها

أضف تصريح نوع مُصدَّر إلى قائمة خصائص المعلومات الخاصة بالهدف. في محرر قائمة الخصائص في Xcode، أنشئ UTExportedTypeDeclarations كمصفوفة تحتوي على قاموس واحد. اجعل قيمة UTTypeIdentifier في ذلك القاموس مساوية لـcom.example.shape-transfer، وقيمة UTTypeDescription مساوية لـنقل شكل اصطناعي، واجعل UTTypeConformsTo مصفوفة تحتوي على public.data. هذا المعرّف توضيحي؛ استخدم معرّفًا فريدًا لتطبيقك الفعلي.

يجب أن يتطابق تصريح Swift مع المعرّف في قائمة الخصائص. تشترط Apple وجود إدخالات مقابلة في قائمة الخصائص للأنواع الموحدة المخصصة، وتشرح التصريحات المُصدَّرة للتنسيقات التي يعرّفها التطبيق. ينقل هذا المثال بيانات لا ملفات، لذا لا يضيف امتدادًا لاسم ملف. Defining file and data types for your app.

أنشئ ShapePayload.swift:

import Foundation
import CoreTransferable
import UniformTypeIdentifiers

extension UTType {
    static let shapeTransfer = UTType(
        exportedAs: "com.example.shape-transfer",
        conformingTo: .data
    )
}

enum ShapeKind: String, Codable, Sendable {
    case circle = "Circle"
    case square = "Square"
}

struct ShapePayload: Codable, Sendable, Transferable {
    let version: Int
    let kind: ShapeKind

    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .shapeTransfer)
            .visibility(.ownProcess)
    }
}

struct PlacedShape: Identifiable {
    let id = UUID()
    let kind: ShapeKind
}

يوفر CodableRepresentation التحويل بين الحمولة والبيانات القابلة للنقل. تحتوي الحمولة على قيمة، لا على واجهة أو مرجع إلى مجموعة قابلة للتغيير. ويتبع تصريح Sendable الصريح قاعدة Swift التي تشترط أن تكون الأعضاء المخزنة في البنية القابلة للإرسال قابلة للإرسال بدورها. Transferable، Sendable.

يوجد تمثيل واحد عن قصد. فإضافة تمثيل نصي بديل ستوفر طريقة أخرى لاستهلاك العنصر، وهذا لا يخدم هذا العقد. توثّق Apple ترتيب التمثيلات وتنسيقات الرجوع الاحتياطية؛ أضفها فقط عندما تحتاج إليها وجهة فعلية. Making a view into a drag source.

اقبل الحمولة ونفّذ تحديثًا واحدًا للمجموعة

أنشئ ShapeTransferApp.swift:

import SwiftUI

@main
struct ShapeTransferApp: App {
    var body: some Scene {
        WindowGroup {
            ShapeTransferView()
        }
    }
}

@MainActor
struct ShapeTransferView: View {
    @State private var placed: [PlacedShape] = []
    @State private var targeted = false
    @State private var message = "Drag a shape into Collection."

    var body: some View {
        HStack(alignment: .top, spacing: 24) {
            VStack(spacing: 16) {
                Text("Palette").font(.headline)
                paletteItem(.circle)
                paletteItem(.square)
            }
            .frame(width: 160)

            VStack(alignment: .leading, spacing: 12) {
                Text("Collection").font(.headline)
                Text(message)
                ForEach(placed) { item in
                    Label(item.kind.rawValue,
                          systemImage: symbol(for: item.kind))
                }
                Spacer(minLength: 20)
            }
            .padding()
            .frame(width: 300, height: 320, alignment: .topLeading)
            .background(targeted ? Color.blue.opacity(0.15)
                                 : Color.gray.opacity(0.08))
            .contentShape(Rectangle())
            .dropDestination(
                for: ShapePayload.self,
                action: { payloads, _ in
                    guard !payloads.isEmpty,
                          payloads.allSatisfy({ $0.version == 1 })
                    else {
                        message = "Drop rejected: unsupported payload."
                        return false
                    }
                    let additions = payloads.map {
                        PlacedShape(kind: $0.kind)
                    }
                    placed.append(contentsOf: additions)
                    message = "Added \(additions.count) shape(s)."
                    return true
                },
                isTargeted: { targeted = $0 }
            )
        }
        .padding(24)
    }

    private func paletteItem(_ kind: ShapeKind) -> some View {
        Label(kind.rawValue, systemImage: symbol(for: kind))
            .padding()
            .background(Color.gray.opacity(0.12))
            .draggable(ShapePayload(version: 1, kind: kind))
    }

    private func symbol(for kind: ShapeKind) -> String {
        switch kind {
        case .circle: return "circle.fill"
        case .square: return "square.fill"
        }
    }
}

يحدّد الاسمان الصريحان action: وisTargeted: التحميل الزائد الأصلي لوجهة الإفلات محددة النوع. وتعرّف Apple القيمة المنطقية المعادة منه بأنها نجاح أو إخفاق، وتكون وسيطة الموضع ضمن نظام إحداثيات واجهة الوجهة. هنا نتجاهل الموضع عمدًا لأن المجموعة تضيف الأشكال بدلًا من تحديد مواقعها. dropDestination(for:action:isTargeted:).

تتحقق قاعدة القبول من كل حمولة مستلمة قبل إضافة أي شيء. يرفض الإصدار غير المدعوم الدفعة المستلمة؛ ويحصل كل محتوى صالح على معرّف جديد في الوجهة. لذلك يؤدي تكرار السحب نفسه من اللوحة إلى إنشاء نسخة أخرى. تنطبق هذه القاعدة على القيم التي تصل إلى الإغلاق؛ أما حالات فشل فك الترميز فتحدث قبل أن تصبح القيم من نوع ShapePayload.

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

استخدم موفّري العناصر عندما يتطلبهم التكامل

يمكن لتنفيذ قائم على الموفّرات نقل الإصدار ونوع الشكل نفسيهما. أبقِ العقد كما هو واستبدل وسيلة النقل باتباع الخطوات التالية:

  1. في إغلاق .onDrag، أنشئ NSItemProvider وسجّل تمثيلًا للبيانات تحت معرّف النوع المخصص نفسه، باستخدام مستوى الرؤية .ownProcess.
  2. التقط قيمة حمولة غير قابلة للتغيير أو بايتات مجهّزة في معالج التحميل. شفّر تلك القيمة واستدعِ معالج اكتمال التمثيل مع البيانات أو الخطأ.
  3. اضبط .onDrop لقبول النوع المخصص.
  4. داخل إجراء الإفلات، ابدأ تحميل كل موفّر مقبول فورًا باستخدام loadDataRepresentation(forTypeIdentifier:completionHandler:).
  5. فك ترميز البيانات المعادة، وتحقق من الإصدار، واجمع النتائج. نفّذ تعديل المجموعة النهائي على الممثل الرئيسي فقط بعد توفر النتائج المطلوبة.

توثّق Apple التسجيل الصريح للبيانات وإمكانية قصر رؤية الموفّرات على التطبيق نفسه. وطريقة التحميل فيها غير متزامنة وتعيد كائن تقدم. يتيح تطابق معرّف النوع محاولة التحميل؛ لكنه لا يضمن نجاح التحميل وفك الترميز. registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:)، NSItemProviderRepresentationVisibility.ownProcess، loadDataRepresentation(forTypeIdentifier:completionHandler:).

هناك حدّان زمنيان مهمان. أولًا، تنص Apple على وجوب بدء تحميل الموفّر داخل إجراء .onDrop، مع إمكان اكتماله لاحقًا. نقل استدعاء التحميل الأولي إلى مهمة لاحقة على ممثل آخر يخالف هذا الشرط الموثق. ثانيًا، تحذّر Apple من إرسال أعمال التصدير إلى الممثل الرئيسي أو الخيط الرئيسي من معالجات التمثيل، لأن ذلك قد يؤدي إلى تعليق. اجعل التصدير مستقلًا عن تعديل الواجهة؛ وحدّث حالة المجموعة بعد اكتمال الاستيراد. onDrop(of:isTargeted:perform:)، onDrag(_:).

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

حافظ على ملكية AppKit للأحداث ودلالات النقل

إذا كانت واجهة AppKit موجودة تتولى تفاعل الفأرة بالفعل، فأبقِ بدء الجلسة فيها. تتطلب beginDraggingSession(with:event:source:) من Apple حدث الضغط على زر الفأرة الذي بدأ التفاعل، وعناصر سحب تستخدم إطاراتها إحداثيات تلك الواجهة، ومصدرًا يطابق NSDraggingSource. تغطي استدعاءات المصدر بدء الجلسة والحركة واكتمالها أو إلغاءها. beginDraggingSession(with:event:source:).

حدود تمثيل AppKit صريحة أيضًا: تتطلب NSDraggingItem(pasteboardWriter:) كائنًا يطابق NSPasteboardWriting. يختلف رد نداء موفّر SwiftUI عن كاتب لوحة لصق AppKit. عند دمج واجهة AppKit مخصصة مع SwiftUI، اترك للواجهة مسؤولية أحداثها وجلساتها، واجعل النموذج المشترك مالك تغييرات المجموعة المقبولة. init(pasteboardWriter:).

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

تتضمن وثائق Apple الحالية DragConfiguration وتحميلًا زائدًا لوجهة إفلات محددة النوع قائمًا على الجلسة، ورصدًا لجلسات السحب في macOS 26. يقترح المصدر العمليات المدعومة، وتختار الوجهة العملية، ويتيح اكتمال الجلسة للمصدر الاستجابة للنتيجة. تحقّق من إتاحة كل رمز في حزمة تطوير البرامج قبل اعتماد هذا المسار. DragConfiguration، dropDestination(for:isEnabled:action:)، onDragSessionUpdated(_:).

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

تحقّق من العقد عبر نتائج يمكن ملاحظتها

استخدم التسلسل التالي للتحقق من التنفيذ على Mac:

  • اسحب Circle إلى Collection: تضيف الوجهة دائرة واحدة، وتحتفظ اللوحة بمدخلها.
  • كرر السحب: تضيف الوجهة عنصرًا ثانيًا مستقلًا بمعرّف خاص به.
  • اسحب Square إلى خارج الوجهة وأفلته: لا تتغير المجموعة.
  • غيّر إصدار حمولة المصدر مؤقتًا إلى 2: ترفض الوجهة الحمولة ولا تضيف شيئًا؛ ثم أعده إلى 1.
  • في تنفيذ قائم على الموفّرات، أعد عمدًا خطأ ترميز أو تحميل: أظهر الإخفاق من دون إدراج دفعة جزئية.
  • في تنفيذ النقل، ألغِ السحب وتأكد من أن العنصر الأصلي لا يزال ضمن مجموعته الأصلية.

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

المصادر