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

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

ما التفاعل الذي يحتاج إليه سطحك؟

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

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

التفاعل الذي يقصده المستخدم نقطة البداية السبب
النقر على لوحة مخصصة معنونة Add circle Button مع تسمية مخصصة يبدأ السطح إجراءً مسمّى.
منح اللوحة خلفية مميزة ومظهرًا عند الضغط ButtonStyle يخصّص المظهر مع الإبقاء على سلوك التفاعل القياسي للزر.
النقر على لوحة رسم لوضع دائرة عند تلك النقطة SpatialTapGesture تتضمن نتيجتها موقع التفاعل.
التعرّف على نقرتين على عرض محتوى دون الحاجة إلى إحداثيات onTapGesture(count: 2) تقبل أداة التعديل عدد النقرات المطلوب.
ضبط أزرار ماوس محددة في تفاعل AppKit NSClickGestureRecognizer يتيح ضبط زر الماوس وعدد النقرات.

توثّق Apple الزر بوصفه إجراءً مقترنًا بتسمية، وتميّز تخصيص المظهر عبر ButtonStyle عن تخصيص التفاعل عبر PrimitiveButtonStyle. كما توثّق واجهات الإيماءات عدد النقرات والنتائج المكانية على نحو منفصل. Button، onTapGesture(count:perform:)، SpatialTapGesture، NSClickGestureRecognizer

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

كيف تنشئ زرًا مخصصًا من دون إعادة بناء سلوكه؟

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

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

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

يتكوّن التطبيق الكامل من هذين الملفين. أولًا، ShapeSurfaceApp.swift:

import SwiftUI
import Foundation

@main
struct ShapeSurfaceApp: App {
    var body: some Scene {
        WindowGroup {
            ShapeSurfaceView()
        }
    }
}

private struct PlacedCircle: Identifiable {
    let id = UUID()
    let center: CGPoint
}

struct ShapeSurfaceView: View {
    @State private var circles: [PlacedCircle] = []
    private let limit = 6

    private var isFull: Bool {
        circles.count >= limit
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Button {
                let x = 35 + CGFloat(circles.count) * 50
                addCircle(at: CGPoint(x: x, y: 100))
            } label: {
                HStack(spacing: 12) {
                    Circle()
                        .fill(Color.blue)
                        .frame(width: 24, height: 24)
                    Text("Add circle")
                }
                .frame(width: 280, height: 64)
                .contentShape(RoundedRectangle(cornerRadius: 12))
            }
            .buttonStyle(SurfaceButtonStyle())
            .keyboardShortcut("a", modifiers: [.command, .shift])
            .disabled(isFull)
            .opacity(isFull ? 0.4 : 1)

            Text("Click the canvas to place a circle. Maximum: 6.")

            ZStack(alignment: .topLeading) {
                Color.gray.opacity(0.1)

                ForEach(circles) { circle in
                    Circle()
                        .fill(Color.blue)
                        .frame(width: 24, height: 24)
                        .position(circle.center)
                }
            }
            .frame(width: 320, height: 200)
            .contentShape(Rectangle())
            .gesture(
                SpatialTapGesture()
                    .onEnded { value in
                        addCircle(at: value.location)
                    }
            )
            .disabled(isFull)
            .border(Color.gray)
            .accessibilityElement(children: .ignore)
            .accessibilityLabel("Circle canvas")
            .accessibilityValue("\(circles.count) of \(limit) circles")

            Text(isFull
                 ? "Six circles placed. Clear to start again."
                 : "\(circles.count) of \(limit) circles placed")

            Button("Clear circles") {
                circles.removeAll()
            }
            .disabled(circles.isEmpty)
        }
        .padding(24)
    }

    private func addCircle(at point: CGPoint) {
        guard !isFull else { return }

        // Keep each 24-point circle inside the 320-by-200 canvas.
        let center = CGPoint(
            x: min(max(point.x, 12), 308),
            y: min(max(point.y, 12), 188)
        )
        circles.append(PlacedCircle(center: center))
    }
}

ثانيًا، SurfaceButtonStyle.swift:

import SwiftUI

struct SurfaceButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .background(
                RoundedRectangle(cornerRadius: 12)
                    .fill(Color.blue.opacity(
                        configuration.isPressed ? 0.25 : 0.1
                    ))
            )
    }
}

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

حدّ الدوائر الست قاعدة توضيحية في التطبيق. ويستخدم مسارا الإدخال كلاهما addCircle(at:)، التي تتحقق من السعة قبل الإضافة. يوضّح المظهر المعطّل الحد، بينما يحافظ الحارس المشترك على اتساق قاعدة التعديل. وتوثّق Apple أن disabled(_:) تتحكم في التفاعل مع العرض، بما في ذلك النقرات والإيماءات. disabled(_:)

لماذا تستخدم لوحة الرسم إيماءة نقر مكانية؟

تحتاج لوحة الرسم إلى معرفة النقطة التي نقر المستخدم عليها. لا يوفّر إصدار onTapGesture(count:perform:) الأساسي سوى إجراء من دون معامل للموقع. أما SpatialTapGesture فتبلّغ عن موقع صراحةً، ويكون المُهيّئ فيها مضبوطًا افتراضيًا على تفاعل واحد في مساحة الإحداثيات المحلية. onTapGesture(count:perform:)، SpatialTapGesture، init(count:coordinateSpace:)

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

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

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

كيف تجعل اختبار النقر يطابق السطح المخصص؟

حدّد مساحة التفاعل المقصودة بعد ضبط حجم التسمية. في المثال، إطار تسمية الزر أبعاده 280 × 64، ويتبعه contentShape مستطيل مستدير الزوايا. وتحدّد لوحة الرسم إطارًا أبعاده 320 × 200، ثم تعلن عن شكل محتوى مستطيل. وتعرّف Apple contentShape(_:eoFill:) بأنه الشكل المستخدم لاختبار النقر. contentShape(_:eoFill:)

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

عندما يتوقف سطح عن الاستجابة، افحص الشروط التالية بالترتيب:

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

كيف تمنع تنافس المعالجات وأخطاء الملكية؟

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

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

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

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

متى تكون AppKit الخطوة التالية المناسبة؟

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

اختر مسار الدمج وفقًا لهدف النشر والمكوّن الذي تملكه:

  • على macOS 26 أو أحدث، يدمج NSGestureRecognizerRepresentable أداة تعرّف AppKit مباشرةً في تسلسل SwiftUI عبر .gesture(...). تثبّت SwiftUI هدف الإجراء؛ نفّذ معالج الإجراء الخاص بالمكوّن القابل للتمثيل واستخدم محوّل السياق لتحويل الإحداثيات. وتحذّر Apple من أن هندسة العرض المرتبط بأداة التعرّف قد تختلف عن هندسة عرض SwiftUI. NSGestureRecognizerRepresentable
  • لعرض AppKit مستضاف في SwiftUI، يدير NSViewRepresentable إنشاء العرض وتحديثه وإنهاءه. استخدم المنسّق لعمليات الاستدعاء، ودع SwiftUI تتحكم في تخطيط العرض الممثّل. تتوفر واجهة البرمجة هذه بدءًا من macOS 10.15. NSViewRepresentable

حافظ على حدود الأحداث عند إضافة أي من المحوّلين. توثّق AppKit أن أداة التعرّف تعمل على العرض المرتبطة به وعلى العروض التابعة له، وتتلقى الأحداث ذات الصلة قبل العرض، وقد تؤخر تسليمها. كما أنها لا تدعم صراحةً تسليم تتبع الأحداث إلى آليات أخرى مثل السحب والإفلات أو القوائم المنبثقة. صمّم هذه التفاعلات باستخدام الآليات المدعومة بدل التعامل مع أداة تعرّف النقر كأنها وسيط عام للأحداث. NSGestureRecognizer

ما الذي ينبغي التحقق منه في تفاعل Mac بعد اكتماله؟

أنشئ المثال في Xcode وتحقق من السلوك الملحوظ قبل تكييفه مع تسلسل أكبر من العروض:

  • انقر على نص الزر المخصص وداخله الفارغ. ينبغي أن يضيف كل تفعيل دائرة واحدة ويحدّث العدد مرة واحدة.
  • اضغط Command–Shift–A في النافذة النشطة. ينبغي أن ينفّذ إجراء الزر المعتمد على الموضع التالي.
  • انقر في مواضع مختلفة على لوحة الرسم، بما في ذلك قرب حوافها. ينبغي أن تتبع الدوائر الإحداثيات المحلية مع الهامش المبيّن في الشيفرة.
  • أضف ست دوائر. ينبغي أن يتوقف كلا مساري الوضع عن الإضافة، مع بقاء Clear circles متاحًا.
  • امسح لوحة الرسم. ينبغي أن يصبح الوضع متاحًا مجددًا.
  • افحص الزر باستخدام VoiceOver واختبر التنقل بلوحة المفاتيح وفق إعدادات Mac الحالية. تحقق من أن الإجراء يظل مفهومًا بعد تطبيق النمط.

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

المصادر