في قائمة عناصر على Mac، اربط التحديد بـ SwiftUI List وأضف إجراءً أساسيًا عبر قائمة سياقية تراعي التحديد. تحدد النقرة الواحدة الصف؛ وتفتحه النقرة المزدوجة. أبقِ حالة التحديد منفصلة عن حالة العرض.

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

استخدم الإجراء الأساسي للحاوية القابلة للتحديد

توثّق Apple صراحةً تحديد العناصر بنقرة واحدة وتنفيذ الإجراءات الأساسية بالنقر المزدوج للحاويات القابلة للتحديد على macOS. نقطة الدخول المدعومة في SwiftUI هي contextMenu(forSelectionType:menu:primaryAction:)، المتاحة بدءًا من macOS 13. أرفقها بحاوية تدعم التحديد، مثل List أو Table. توثيق Apple للقائمة السياقية التي تراعي التحديد.

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

اختر التنفيذ بحسب طريقة العرض التي تملك التفاعل:

واجهتك نقطة بداية مناسبة المسؤولية التي تبقى عليك
قائمة أو جدول عناصر بـ SwiftUI على macOS 13 أو أحدث رابط التحديد مع إجراء أساسي يراعي التحديد حل معرّفات الإجراءات وعرض المحتوى المقابل
لوحة أو شكل مخصص بـ SwiftUI تركيب صريح للإيماءات مظهر التحديد وسياسة التنشيط ومسارات الإدخال الأخرى
جدول AppKit مضمّن في SwiftUI تحديد NSTableView وdoubleAction ربط التحديد والتنشيط بحالة SwiftUI
سطح رسم مخصص بـ AppKit معالجة أحداث الفأرة الخاصة به اختبار استهداف النقرات وهوية العنصر وسياسة تسلسل النقرات

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

أنشئ مثالًا كاملًا لتحديد عنصر واحد

أنشئ مشروع تطبيق macOS في Xcode بواجهة SwiftUI واضبط هدف النشر على macOS 13 أو أحدث. استبدل تعريفات التطبيق وعرض المحتوى التي أنشأها المشروع بالملف الواحد التالي. أبقِ تعريفًا واحدًا فقط لتطبيق @main في الهدف.

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

import SwiftUI

struct SampleItem: Identifiable {
    let id: Int
    let name: String
    let tint: Color
}

@main
struct ClickSelectionDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ItemBrowser()
        }
    }
}

struct ItemBrowser: View {
    private let items = [
        SampleItem(id: 1, name: "Circle A", tint: .blue),
        SampleItem(id: 2, name: "Circle B", tint: .orange),
        SampleItem(id: 3, name: "Circle C", tint: .purple)
    ]

    @State private var selectedID: SampleItem.ID? = nil
    @State private var openedItem: SampleItem? = nil
    @State private var message = "Select an item."

    private var selectedItem: SampleItem? {
        items.first { $0.id == selectedID }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Text(selectedItem.map {
                    "Selected: \($0.name)"
                } ?? "No selection")
                Spacer()
                Button("Open") {
                    if let id = selectedID {
                        openItems([id])
                    }
                }
                .disabled(selectedItem == nil)
            }

            List(items, selection: $selectedID) { item in
                HStack(spacing: 10) {
                    Circle()
                        .fill(item.tint)
                        .frame(width: 18, height: 18)
                    Text(item.name)
                }
                .padding(.vertical, 4)
            }
            .contextMenu(forSelectionType: SampleItem.ID.self) { ids in
                if ids.count == 1 {
                    Button("Open") {
                        openItems(ids)
                    }
                }
            } primaryAction: { ids in
                openItems(ids)
            }

            Text(message)
                .font(.caption)
        }
        .padding()
        .frame(minWidth: 360, minHeight: 280)
        .sheet(item: $openedItem) { item in
            VStack(spacing: 16) {
                Circle()
                    .fill(item.tint)
                    .frame(width: 80, height: 80)
                Text(item.name)
                    .font(.title2)
                Button("Close") {
                    openedItem = nil
                }
            }
            .padding(32)
            .frame(minWidth: 260, minHeight: 220)
        }
    }

    private func openItems(_ ids: Set<SampleItem.ID>) {
        guard ids.count == 1, let id = ids.first else {
            message = "Choose exactly one item to open."
            return
        }
        guard let item = items.first(where: { $0.id == id }) else {
            message = "That item is no longer available."
            return
        }
        openedItem = item
        message = "Opened: \(item.name)"
    }
}

تم فحص التعريفات والمراجع، لكن لم يُصرّف هذا المثال ولم يُشغّل هنا: فالبيئة المتاحة لا تتضمن سلسلة أدوات Swift أو واجهة رسومية لنظام macOS. أنشئه في Xcode ونفّذ اختبارات التفاعل أدناه.

أبقِ الهوية والتحديد والفتح منفصلة

يلتزم SampleItem ببروتوكول Swift المسمى Identifiable. يمثّل id الثابت العنصر نفسه عبر تحديثات العرض. وتعرّف Apple هذا البروتوكول بأنه يستند إلى هوية مستقرة؛ أما اسم العرض فهو خاصية منفصلة. في تطبيق أكبر، حافظ على معرّف العنصر عند تغيير اسمه أو مظهره. توثيق Apple لـ Identifiable.

ينشئ selectedID الاختياري قائمة تسمح بتحديد عنصر واحد. أما مجموعة من المعرّفات فستتيح تحديد عدة عناصر. يصف توثيق Apple للقائمة هذين الشكلين للرابط ومظهر التحديد الذي توفره الحاوية. توثيق Apple لـ List.

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

حلّ المعرّفات الممرّرة إلى الإجراء

تأخذ واجهة برمجة التطبيقات للقائمة السياقية نوع معرّف الحاوية، ويتلقى إجراءها مجموعة من المعرّفات. لذا يستخدم المثال SampleItem.ID.self، بما يطابق تحديد القائمة. وهو يحلّ وسيطة الاستدعاء الراجع بدلًا من استبدالها بقيمة تحديد جرى قراءتها على نحو منفصل. توثيق Apple لمعاملات القائمة السياقية.

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

لماذا يتطلب معالجا النقر تصميمًا مختلفًا

قد تحتاج لوحة مخصصة إلى إيماءاتها الخاصة. تقبل onTapGesture(count:perform:) في SwiftUI عددًا مطلوبًا من النقرات. لكن توثيقها لا يثبت أن وضع معدّلين مستقلين على عرض ما ينشئ عقدًا حصريًا للنقر المفرد مقابل النقر المزدوج. توثيق Apple لإجراء النقر.

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

تتيح SwiftUI للسطح المخصص exclusively(before:): لا ينجح سوى مكوّن واحد من الإيماءة، وتكون الأولوية للأول. المقطع التالي تعبير عن إيماءة، وليس بديلًا كاملًا لمثال القائمة: توثيق Apple للإيماءة الحصرية.

TapGesture(count: 2)
    .exclusively(before: TapGesture(count: 1))

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

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

وفي اللوحة المخصصة، حدّد أيضًا المساحة القابلة للنقر المقصودة. يمكن لـ contentShape(Rectangle()) جعل منطقة مستطيلة مبطّنة تشارك في اختبار استهداف النقرات؛ لكنه لا يحدد أسبقية الإيماءات. أبقِ هذين الأمرين منفصلين عند تشخيص النقرات التي لا تُلتقط. توثيق Apple لشكل المحتوى.

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

إذا كان جدول AppKit موجود يملك الصفوف، فاستخدم آلية التنشيط المدعومة في ذلك الجدول. يرسل NSTableView.doubleAction محدِّدًا إلى هدفه عند النقر المزدوج. وتوثّق Apple أيضًا clickedRow وclickedColumn لتحديد موضع حدوث التنشيط، بما في ذلك عناوين الأعمدة. تحقّق من أن الموضع يشير إلى عنصر قبل فتحه. توثيق Apple لـ doubleAction.

في سطح AppKit مخصص، يتلقى mouseDown(with:) حدث ضغط زر الفأرة الأيسر، ويبلّغ NSEvent.clickCount عن عدد النقرات المرتبط بأحداث الضغط على زر الفأرة أو رفعه. وتقع قراءة هذه الخاصية في حدث غير ذي صلة خارج نطاق عقدها الموثّق، وقد تؤدي إلى استثناء. أبقِ تفسيرها ضمن العرض الذي يعالج حدث الفأرة المناسب. توثيق Apple للضغط على زر الفأرة، توثيق Apple لعدد النقرات.

استخدم NSViewRepresentable عند تضمين عرض AppKit الذي تملكه في SwiftUI. أنشئه في makeNSView، وزامن الإعدادات الحالية في updateNSView، واستخدم منسقًا عندما يحتاج التواصل عبر الهدف والإجراء أو المفوّض إلى جسر. توثّق Apple الإنشاء والتحديث والتنسيق وإنهاء دورة الحياة بوصفها أجزاءً من هذا التكامل. توثيق Apple لـ NSViewRepresentable.

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

تحقّق من التحديد والفتح بوصفهما نتيجتين منفصلتين

نفّذ الخطوات التالية بعد إنشاء المثال:

  1. انقر على Circle A مرة واحدة. ينبغي تحديد صفه، وأن يذكره وسم التحديد، وألا تظهر أي ورقة.
  2. انقر نقرًا مزدوجًا على Circle B. تأكد من أن ورقة التفاصيل تعرض Circle B.
  3. أغلق الورقة وانقر على Circle C مرة واحدة. تأكد من تغيّر التحديد من دون فتح ورقة أخرى.
  4. فعّل الزر الظاهر Open. تأكد من أنه يفتح العنصر المحدد عبر المعالج نفسه.
  5. استدعِ الأمر السياقي Open لأحد الصفوف، وتحقق من العنصر الذي يفتحه.
  6. انقر مرة ومزدوجًا على مساحة فارغة في القائمة. تأكد من أن ذلك لا يفتح عنصرًا غير ذي صلة.
  7. إذا أضفت عناصر تحكم مضمّنة أو سحبًا أو تراكبات، فأعد الاختبارات فوق تلك المناطق المحددة.

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

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

المصادر