أبقِ الموضع المعتمد للشكل دون تغيير أثناء معاينة SwiftUI للسحب، ثم اعتمد الإزاحة النهائية في onEnded. يحافظ ذلك على موضع البداية حتى اكتمال الإيماءة من دون الاعتماد على توقيت إفلات زر الماوس في AppKit. إليك مثالًا كاملًا لتحريك مستطيل في تطبيق Mac خاص بك.
ما الحد الفاصل للاكتمال الذي ينبغي أن يملك حالة البداية؟
في تفاعل تحريك شكل، تتمثل المهمة المحددة في حساب كل معاينة والموضع النهائي انطلاقًا من موضع البداية نفسه. حد الاكتمال المناسب هو اكتمال إيماءة ذلك الشكل بنجاح. توضح Apple أن onEnded(_:) يوفّر القيمة النهائية للإيماءة ولا يعمل إلا عند انتهائها بنجاح. وتوصي باستخدام @GestureState للحالة المؤقتة التي يجب أن تُعاد إلى وضعها الأولي بصرف النظر عن كيفية انتهاء الإيماءة. وثائق Apple حول onEnded(_:).
امنح التفاعل نوعين من الحالة: موضعًا معتمدًا في النموذج وإزاحة معاينة مؤقتة. أثناء السحب، اعرض مجموعهما. وعند اكتمال الإيماءة بنجاح، أضف الإزاحة النهائية إلى الموضع المعتمد مرة واحدة. هذا تصميم مستنبط من التمييز الذي تضعه Apple بين تحديثات الإيماءة المؤقتة وتحديثات الحالة الدائمة. إضافة التفاعلية باستخدام الإيماءات.
| المعلومات | الجهة المالكة في هذا المثال | مدة البقاء |
|---|---|---|
| المركز المعتمد للمستطيل | نموذج @State خاص |
يستمر بعد اكتمال الإيماءة |
| إزاحة المعاينة الحالية | @GestureState |
تُعاد عند خمول الإيماءة |
| الإزاحة النهائية | وسيط رد النداء onEnded |
تُستخدم تزامنيًا لاعتماد الحركة |
| حدث إفلات زر الماوس في AppKit | معالجة أحداث AppKit | يُبلغ عن إفلات زر؛ ولا يحدد اكتمال إيماءة SwiftUI |
توضح Apple أن @State هو تخزين تديره SwiftUI ومصدر للحقيقة للقيم في تسلسل العرض الهرمي. وتُعاد @GestureState تلقائيًا عند خمول الإيماءة المرتبطة بها. إن إبقاء خط الأساس في الأولى والمعاينة في الثانية يمنح كل قيمة مدة البقاء التي يحتاج إليها هذا التفاعل. State، GestureState.
أنشئ مثالًا كاملًا لسحب مستطيل على Mac
أنشئ تطبيق SwiftUI لنظام macOS في Xcode واجعل الحد الأدنى له macOS 14 أو أحدث. استبدل تعريف التطبيق وعرض المحتوى اللذين أنشأهما Xcode بالملف الوحيد التالي، مع التأكد من وجود تعريف واحد فقط لـ @main في الهدف. يستخدم المثال مستطيلًا اصطناعيًا واحدًا مخزنًا بالكامل في الذاكرة. وتوثّق Apple أن واجهات مساحات الإحداثيات المسماة فيه متاحة بدءًا من macOS 14. مهيئ إيماءة السحب، مساحات الإحداثيات المسماة.
import SwiftUI
@main
struct GestureStartDemoApp: App {
var body: some Scene {
WindowGroup {
ShapeCanvas()
}
}
}
private struct SampleItem {
var center = CGPoint(x: 160, y: 140)
}
private struct ShapeCanvas: View {
@State private var item = SampleItem()
@GestureState private var previewTranslation = CGSize.zero
private let canvasWidth: CGFloat = 600
private let canvasHeight: CGFloat = 360
private let shapeWidth: CGFloat = 80
private let shapeHeight: CGFloat = 60
private func projectedCenter(_ translation: CGSize) -> CGPoint {
let proposedX = item.center.x + translation.width
let proposedY = item.center.y + translation.height
let halfWidth = shapeWidth / 2
let halfHeight = shapeHeight / 2
return CGPoint(
x: min(max(proposedX, halfWidth),
canvasWidth - halfWidth),
y: min(max(proposedY, halfHeight),
canvasHeight - halfHeight)
)
}
private var moveGesture: some Gesture {
DragGesture(
minimumDistance: 3,
coordinateSpace: .named("shapeCanvas")
)
.updating($previewTranslation) { value, translation, _ in
translation = value.translation
}
.onEnded { value in
let finalCenter = projectedCenter(value.translation)
item.center = finalCenter
}
}
var body: some View {
let displayedCenter = projectedCenter(previewTranslation)
VStack(alignment: .leading, spacing: 12) {
Text("Drag the blue rectangle")
.font(.headline)
ZStack {
Color.gray.opacity(0.12)
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue)
.frame(width: shapeWidth, height: shapeHeight)
.contentShape(Rectangle())
.gesture(moveGesture)
.position(
x: displayedCenter.x,
y: displayedCenter.y
)
}
.frame(width: canvasWidth, height: canvasHeight)
.coordinateSpace(.named("shapeCanvas"))
.clipped()
Text(
"Committed center: " +
"\(Int(item.center.x)), \(Int(item.center.y))"
)
.monospacedDigit()
}
.padding(20)
}
}
الثابت الأساسي هو أن item.center لا يتغير مطلقًا في updating. فهو يظل خط أساس النموذج للإيماءة حتى تحسب onEnded القيمة finalCenter. وتقرأ العملية الحسابية النهائية value.translation مباشرة، لذلك لا تحتاج إلى احتفاظ خاصية المعاينة بآخر قيمة لها أثناء الإكمال.
تقيس الإيماءة الحركة في مساحة الإحداثيات المسماة للوحة الحاوية. وتضع position(x:y:) الخاصة بالمستطيل مركزه في مساحة العنصر الأب نفسها. ويساعد تطابق المساحتين على تجنب خلط الإحداثيات المحلية للشكل المتحرك بإحداثيات اللوحة. توثّق Apple قياس الإيماءة في المساحة المسماة والتموضع اعتمادًا على المركز. coordinateSpace(_:)، position(x:y:).
كما تحافظ الدالة المشتركة projectedCenter على بقاء المستطيل داخل اللوحة. تستخدم المعاينة والاعتماد الحدود نفسها: مراكز أفقية من 40 إلى 560، ومراكز رأسية من 30 إلى 330. عند الحافة، تظل الحركة مبنية على الإزاحة الكلية من بداية السحب الأصلية؛ ويبتعد المستطيل عن الحافة عندما يعود الموضع المقترح إلى داخل الحدود.
لا تُعد الحركة التي تقل عن عتبة التعرّف المضبوطة عند 3 نقطة بدايةً لسحب معرّف. يتعامل هذا المثال عمدًا مع البداية على أنها بدء تفاعل معرّف لتحريك الشكل. أما اشتراط التقاط حالة النموذج عند حدث الضغط الفعلي على زر الماوس فهو حد فاصل مختلف. مهيئ إيماءة السحب.
لماذا يبقى موضع البداية متاحًا أثناء التحديث النهائي
تعرّف Apple قيمة DragGesture.Value.translation بأنها الإزاحة الكلية من بداية الإيماءة حتى الحدث الحالي. فهي تراكمية، وليست فرقًا منذ استدعاء رد النداء السابق. لذلك فإن إضافة كل إزاحة مُبلّغ عنها إلى موضع نموذج سبق تحديثه ستؤدي إلى احتساب الحركة أكثر من مرة. DragGesture.Value.
إليك مدخلات توضيحية صريحة، تقع جميعها داخل حدود اللوحة:
| المرحلة | الإزاحة الكلية المُبلّغ عنها | المركز المعتمد | المركز المعروض |
|---|---|---|---|
| قبل السحب | (0, 0) |
(160, 140) |
(160, 140) |
| التحديث الأول | (12, 5) |
(160, 140) |
(172, 145) |
| التحديث الثاني | (30, -10) |
(160, 140) |
(190, 130) |
| انتهاء ناجح | (45, 20) |
يصبح (205, 160) |
يستقر عند (205, 160) |
تستخدم كل معاينة خط الأساس نفسه. ويطبّق الإكمال الإزاحة النهائية (45, 20) مرة واحدة. بعد ذلك، يبدأ سحب ناجح آخر بإزاحة (-15, 10) من (205, 160) ويعتمد (190, 170). وقد تحقّق من هذه الحسابات باستخدام حساب حتمي في Python. وفُحص تطبيق SwiftUI استنادًا إلى وثائق واجهات البرمجة المسترجعة، لكن لم يُترجم التطبيق أو يُشغّل في هذه البيئة.
لا تستبدل startLocation بمركز بداية المستطيل. تعرّف Apple startLocation على أنها موقع أول حدث للسحب. قد يبدأ المؤشر قرب زاوية المستطيل، لذا قد يختلف ذلك الموقع عن مركز الشكل في النموذج. وبالمثل، فإن predictedEndTranslation تنبؤ يعتمد على السرعة؛ استخدم translation النهائية في تفاعل التموضع المباشر هذا. DragGesture.Value.
لا يتطلب الإلغاء التراجع عن الموضع المعتمد في هذا التصميم، لأن تحديثات المعاينة لا تستبدله. توثّق Apple أن حالة الإيماءة تُعاد عند خمول الإيماءة، بينما لا تعمل onEnded إلا بعد النجاح. لذلك يختفي الإزاحة المؤقتة من دون اعتماد حركة أُلغيت. وهذا الاستنتاج مستند إلى السلوكيات الموثقة؛ ولا يفترض ترتيبًا بين إعادة الضبط ورد نداء الإكمال الناجح. GestureState، onEnded(_:).
لماذا لا ينبغي لمراقب إفلات زر الماوس في AppKit مسح خط الأساس
يتلقى مراقب الأحداث المحلي في AppKit الأحداث قبل إرسالها إلى التطبيق. لذلك قد تؤدي إزالة حالة بدء الإيماءة المشتركة من حدث إفلات زر الماوس الذي يراقبه إلى حذفها قبل أن تعالج المعالجة اللاحقة ذلك الحدث. وهذه نتيجة لحد الإرسال الموثق، وليست ضمانًا موثقًا لترتيب ردود نداء SwiftUI. addLocalMonitorForEvents(matching:handler:).
وبالمثل، تُبلغ NSResponder.mouseUp(with:) عن إفلات المستخدم زر الماوس الأيسر. ولا ينص عقدها على اكتمال كل ردود نداء الإيماءات أو تحديثات العرض في SwiftUI. أبقِ مراقبة الأحداث الخام واكتمال الإيماءة المتعرّف عليها مسؤوليتين منفصلتين. mouseUp(with:).
لا تثبت واجهات البرمجة التي روجعت هنا وجود معنى عام لرد نداء يفيد بأن SwiftUI أنهت كل المعالجة المرتبطة بحدث خام. إن إضافة مهمة تنظيف إلى قائمة الانتظار أو مؤقت قصير يضيف افتراضًا متعلقًا بالجدولة من دون إثبات هذه العلاقة. لهذه المهمة، المسار المدعوم هو إجراء الحساب النهائي للنموذج داخل onEnded، حيث تتوفر قيمة الإيماءة النهائية.
إذا كان لا بد من تنفيذ التفاعل في عرض مخصص لـ AppKit، فليتولَّ ذلك العرض تسلسل الإدخال وحالة الجلسة، وادمجه عبر NSViewRepresentable. توفر Apple نقاط ربط للإنشاء والتحديث والمنسّق والتفكيك لهذا الدمج. احتفظ بجلسة العرض المُمثّل في عرض AppKit أو في المنسّق الخاص به بدلًا من إعادة بنائها من كل قيمة للعنصر المُمثّل. تتحكم SwiftUI في تخطيط العرض المُمثّل، لذا لا تغيّر إطاره أو حدوده التي تديرها بنفسك. NSViewRepresentable.
متى يحتاج التفاعل إلى لقطة صريحة؟
يكفي الموضع المعتمد في المثال لأن أي إجراء آخر لا يغيّره أثناء نشاط السحب. في لوحة أكبر، استخدم دليل القرار هذا قبل إضافة كائن جلسة منفصل:
| المتطلب | نتيجة التصميم |
|---|---|
| تحريك عنصر واحد يظل موضعه المعتمد دون تغيير أثناء المعاينة | استخدم نهج خط الأساس والإزاحة المؤقتة المعروض |
| تحريك عدة عناصر محددة معًا | احتفظ بالمعرّفات الثابتة ومواضع البداية كلقطة واحدة للتفاعل |
| السماح لأوامر أخرى بتعديل المواضع أثناء السحب | حدّد ما إذا كان ينبغي منع هذه التعديلات أو إلغاء السحب أو معالجة التغيير وفق مراجعة النموذج |
| التقاط الحالة عند الضغط الفعلي على زر الماوس قبل التعرّف على الإيماءة | استخدم حد إدخال في AppKit تملكه بوضوح؛ فالتحديث الأول للإيماءة لا يعادله |
| الاحتفاظ بجلسة منفصلة بعد الإكمال | عيّن لها مالكًا وسياسة إنهاء صريحة بدلًا من الاعتماد على حالة الإيماءة المؤقتة |
يجب أن تحافظ اللقطة على القيم. تتمتع هياكل Swift بدلالات القيمة، بينما يؤدي إسناد مثيل فئة إلى نسخ مرجعه. لذلك فإن مصفوفة من المراجع إلى كائنات عناصر قابلة للتغيير لا تجمّد مواضعها بحد ذاتها. عند تحريك عدة عناصر، التقط المعرّفات وقيم الإحداثيات اللازمة للحساب النهائي. الهياكل والفئات — لغة البرمجة Swift.
إذا أضفت جلسة مستمرة، فحدّد صراحةً ما يحدث عند الإكمال الناجح والإلغاء وإزالة العرض. إن مسحها فقط في onEnded يترك الإلغاء دون معالجة، لأن رد النداء هذا لا يعمل إلا عند النجاح. لا تُعِد توظيف معاملة إعادة ضبط حالة الإيماءة بوصفها إشعارًا عامًا بالاكتمال: توثّق Apple أن إغلاق إعادة الضبط يوفّر معاملة لإعادة ضبط حالة الإيماءة. onEnded(_:)، init(wrappedValue:reset:).
في مهمة تحريك المستطيل المعتادة، حافظ على خط الأساس بترك نموذج الموضع المعتمد دون تغيير أثناء المعاينة. احسب الموضع النهائي من قيمة الإيماءة الناجحة نفسها، واعتمده مرة واحدة، ودع حالة الإيماءة المؤقتة تُعاد. يمنح ذلك موضع البداية مالكًا ومدة بقاء واضحين حتى انتهاء الحساب الذي يحتاج إليه.
المصادر
- إضافة التفاعلية باستخدام الإيماءات — Apple.
- State — Apple.
- GestureState — Apple.
- onEnded(_:) — Apple.
- DragGesture.Value — Apple.
- init(minimumDistance:coordinateSpace:) — Apple.
- coordinateSpace(_:) — Apple.
- position(x:y:) — Apple.
- addLocalMonitorForEvents(matching:handler:) — Apple.
- mouseUp(with:) — Apple.
- NSViewRepresentable — Apple.
- الهياكل والفئات — لغة البرمجة Swift — مشروع Swift.org.
- init(wrappedValue:reset:) — Apple.