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

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

لماذا يستجيب النص بينما لا تستجيب المساحة الفارغة؟

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

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

استخدم .frame(maxWidth: .infinity, alignment: .leading) للسماح للتسمية بالتمدد عندما يقترح العنصر الأب عرضاً إضافياً. ثم طبّق .contentShape(.interaction, Rectangle()) على تلك التسمية الموسّعة. وتعرّف Apple نوع شكل المحتوى interaction خصيصاً لاختبار hit testing وإمكانية الوصول.

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

أنشئ مثالاً متكاملاً لزر صف

أنشئ مشروع تطبيق macOS في Xcode باستخدام SwiftUI وSwift. اضبط هدف النشر على macOS 12 أو أحدث لاستخدام صيغة التحميل الزائد الصريحة لشكل المحتوى .interaction، التي تذكر Apple أن توفرها يبدأ من macOS 12. استبدل تعريف التطبيق وعرض المحتوى اللذين أُنشئا تلقائياً بالبرنامج ذي الملف الواحد أدناه؛ وأبقِ على تعريف تطبيق واحد فقط يتضمن @main. راجع توثيق صيغة التحميل الزائد لشكل المحتوى من Apple لمعرفة التوفر.

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

import SwiftUI

@main
struct ClickableRowsApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct SampleItem: Identifiable {
    let id: Int
    let title: String
    let color: Color
}

struct ContentView: View {
    private let items = [
        SampleItem(id: 1, title: "Blue square", color: .blue),
        SampleItem(id: 2, title: "Orange square", color: .orange),
        SampleItem(id: 3, title: "Purple square", color: .purple)
    ]

    @State private var lastActivated = "None"
    @State private var activationCount = 0

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Choose a shape")
                .font(.headline)

            ScrollView {
                VStack(spacing: 8) {
                    ForEach(items) { item in
                        Button {
                            lastActivated = item.title
                            activationCount += 1
                        } label: {
                            HStack(spacing: 12) {
                                Rectangle()
                                    .fill(item.color)
                                    .frame(width: 24, height: 24)
                                Text(item.title)
                            }
                            .padding(.horizontal, 12)
                            .padding(.vertical, 10)
                            .frame(maxWidth: .infinity,
                                   alignment: .leading)
                            .contentShape(.interaction, Rectangle())
                        }
                        .buttonStyle(.plain)
                        .background(Color.gray.opacity(0.12))
                    }
                }
            }

            Text("Last activated: \(lastActivated)")
            Text("Activations: \(activationCount)")
        }
        .padding(16)
        .frame(minWidth: 360, minHeight: 280)
    }
}

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

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

تمت مراجعة هذا البرنامج بالاستناد إلى وثائق واجهات البرمجة المسترجعة، لكن لم يُجمّع أو يُشغّل هنا لعدم توفر سلسلة أدوات Swift. تحقّق من سلوك المؤشر ولوحة المفاتيح في هدف macOS لديك قبل دمجه.

أين ينبغي وضع الإطار والحشوة وشكل المحتوى؟

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

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

يحافظ المعدّل .buttonStyle(.plain) على بساطة مظهر المثال. تصف Apple نمط الزر plain بأنه بلا زخرفة عندما يكون خاملاً، مع احتمال ظهور تأثيرات عند الضغط أو التركيز أو التمكين. هذا خيار تنسيق، وليس بديلاً عن تحديد عرض التسمية وشكل محتواها.

إذا كان هدف النشر أقدم من macOS 12، فاستبدل .contentShape(.interaction, Rectangle()) بـ .contentShape(Rectangle()). توثّق Apple أن صيغة التحميل الزائد الأقدم تحدد شكل اختبار hit testing، وتذكر أن توفرها يبدأ من macOS 10.15. يغيّر هذا الاستبدال متطلبات واجهة برمجة شكل المحتوى؛ تحقّق من واجهات برمجة التطبيقات الأخرى في تطبيقك كلّاً على حدة.

هل ينبغي أن يستخدم الصف زراً أم تحديد عنصر في قائمة أم إيماءة؟

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

السلوك الظاهر للقارئ نقطة بداية مناسبة الحد الذي ينبغي الحفاظ عليه
ينفّذ الصف بأكمله إجراءً واحداً Button بتسمية موسّعة ذات شكل محدد اجعل للصف مالكاً واحداً للإجراء
يمثّل الصف التحديد الحالي List مع ربط للتحديد دع القائمة تتولى سلوك التحديد
يحتاج عرض مخصّص إلى إيماءة نقر مستقلة عرض ذو شكل محدد مع onTapGesture اربط الإيماءة بالمنطقة المقصودة
يحتوي الصف على عدة عناصر تحكم مستقلة عناصر تحكم شقيقة منفصلة خصّص لكل عنصر تحكم نطاقه الخاص

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

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

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

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

شخّص صفاً ما زال لا يستجيب للنقر

اتبع التسلسل التالي على أصغر نسخة من صفك. فهو يفصل مشكلة تحديد الحجم عن مشكلة شكل المنطقة المستهدفة وعن مشكلة العرض المتداخل.

  1. تحقق من الحدود الفعلية للتسمية. أضف مؤقتاً .border(Color.orange) مباشرةً بعد معدّل إطار التسمية. إذا انتهى الإطار بجوار النص، فتحقق من اقتراح العرض الذي يقدمه العنصر الأب ومن أي قيود عرض ثابتة.
  2. تحقق من موضع المعدّلات. تأكد من أن شكل المحتوى يغطي التسمية الموسّعة، بما يشمل الحشوة التي تريد جعلها قابلة للنقر.
  3. تحقق من مالك التفاعل. استخدم إجراء الزر في صف الزر. أزل إيماءات النقر المكررة على الصف أثناء عزل المشكلة.
  4. تحقق من الزخارف المتداخلة. افحص الطبقات العلوية والعروض الشقيقة فوق الصف. بالنسبة إلى الزخرفة التي ينبغي ألا تستقبل إدخالاً مطلقاً، فكّر في استخدام .allowsHitTesting(false) على تلك الزخرفة وحدها.
  5. أعِد عناصر التحكم الأخرى واحداً تلو الآخر. اختبر نطاق كل عنصر تحكم بعد إعادته، ثم اختبر المساحة الفارغة المتبقية.

توثّق Apple أن allowsHitTesting(_:) يتحكم في مشاركة العرض في عمليات hit testing. سيؤدي تطبيقه على الصف بأكمله إلى استبعاده من المشاركة؛ لذا استهدف العرض الزخرفي المحدد عندما يكون ذلك هو السلوك المقصود.

استخدم بضعة مواضع نقر مختلفة لتأكيد النتيجة. يصف الجدول السلوك المتوقع للبرنامج المقدّم، ولا يعرض نتائج اختبارات مقاسة.

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

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

متى يصبح اختبار hit testing في AppKit مهماً؟

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

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

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

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

المصادر