عند انتهاء السحب على لوحة رسم، احسب الهندسة النهائية للعنصر ضمن نظام إحداثيات الهدف. اختبر الاحتواء الكامل أولًا، ثم التداخل ذي المساحة الموجبة. يطبّق هذا البرنامج التعليمي هذه القاعدة على العناصر المستطيلة في تطبيق Mac خاص بك، ويشرح المقابل في AppKit.
حدّد ما يُعدّ تداخلًا واحتواءً
تخيّل لوحة رسم بسيطة لسطح المكتب تحتوي على مستطيل أزرق قابل للتحريك وهدف رمادي ثابت. تتمثل مهمة المستخدم في تحديد ما إذا كان العنصر المُفلت يقع بالكامل داخل الهدف، أو يتداخل معه جزئيًا، أو لا يشترك معه في أي مساحة. جميع البيانات في المثال اصطناعية ومخزنة في الذاكرة.
الاحتواء اتجاهي: يجب أن يحتوي الهدف العنصر. توثّق Apple أن CGRectContainsRect(target, item) تُرجع true عندما يساوي اتحاد المستطيلات الهدف. ويشمل ذلك عنصرًا يطابق الهدف تمامًا. يمنع التحقق من الاحتواء أولًا الإبلاغ عن عنصر محتوى بالكامل على أنه تداخل جزئي. توثيق CGRectContainsRect
في هذا المثال، يتطلب التداخل وجود منطقة مشتركة ذات عرض موجب وارتفاع موجب. أما مجرد ملامسة حافة أو زاوية فتُعدّ لا يوجد تداخل مساحي. احسب التقاطع وافحص أبعاده بدلًا من ترك سلوك التلامس ضمنيًا. توثّق Apple أن CGRectIntersection تُرجع المستطيل المشترك، أو مستطيلًا فارغًا عند عدم وجود تقاطع. توثيق CGRectIntersection
| النتيجة | القاعدة المستخدمة في هذا البرنامج التعليمي | مثال |
|---|---|---|
محتوى |
يحتوي الهدف العنصر بالكامل، بما في ذلك الحدود المتطابقة | يقع العنصر داخل الهدف أو يطابقه تمامًا |
تداخل |
لا يتحقق الاحتواء، لكن المنطقة المشتركة ذات عرض وارتفاع موجبين | يتجاوز العنصر حافة الهدف |
لا يوجد تداخل مساحي |
لا يوجد تقاطع ذو مساحة موجبة | العنصر منفصل أو يلامس حافة فقط |
هندسة غير صالحة |
يحتوي أحد المستطيلين على إحداثيات غير منتهية أو أبعاد غير موجبة | عنصر عرضه صفر |
هذه قواعد خاصة بالتطبيق مستمدة من عمليات الهندسة. إذا كانت واجهتك تتطلب هامشًا داخل الهدف، فحدّد هذا الهامش على نحو مستقل واختبر مقابل مستطيل قبول أصغر. اجعل الهدف المرئي وقاعدة القبول مفهومين للشخص الذي يسحب العنصر.
استخدم نظام إحداثيات واحدًا وإزاحة السحب النهائية
يجب أن يعبّر المستطيل الذي ترسمه والمستطيل الذي تصنّفه عن الموضع نفسه. في هذا المثال، يستخدم كل من العنصر والهدف إحداثيات اللوحة. تُسنَد إلى اللوحة مساحة إحداثيات مسماة، ويستخدم DragGesture الخاص بالعنصر الاسم نفسه. يدعم معدّل coordinateSpace(_:) الحالي من Apple هذا النمط على macOS 14 والإصدارات الأحدث. توثيق مساحة الإحداثيات المسماة
خزّن مستطيل العنصر المعتمد منفصلًا عن إزاحة السحب المؤقتة. أثناء الإيماءة، ارسم المستطيل المعتمد بعد إزاحته بمقدار الترجمة. عند اكتمال الإيماءة بنجاح، احسب ذلك المستطيل النهائي مجددًا باستخدام قيمة الاستدعاء الراجع، وصنّفه، ثم اعتمده مرة واحدة.
تعرّف Apple translation بأنها الإزاحة الكلية من بداية السحب إلى الحدث الحالي. لذلك، فإن إضافة الترجمة الكاملة مرارًا إلى موضع سبق تحديثه ستؤدي إلى تراكم حركة أكبر من اللازم. طبّقها على الموضع المعتمد الذي لم يتغير. توثيق ترجمة السحب
لا تكفي نقطة الإفلات وحدها لتحديد ما إذا كان العنصر بأكمله يقع داخل الهدف. فقد يمسك المستخدم المستطيل بالقرب من إحدى زواياه، فيبقى جزء كبير منه خارجه حتى عندما يكون المؤشر بالداخل. كما أن تطبيق الترجمة يحافظ على موضع الإمساك الأصلي من دون نقل مركز العنصر إلى المؤشر.
أنشئ مثالًا كاملًا لمستطيل في SwiftUI
أنشئ تطبيق SwiftUI لنظام macOS في Xcode، واجعل هدف النشر macOS 14 أو إصدارًا أحدث. استبدل نقطة دخول التطبيق التي أنشأها Xcode بالكتلتين أدناه، وضعهما متتاليتين في ملف Swift واحد. أزل نقطة دخول التطبيق الأصلية كي لا يتضمن الهدف سوى تصريح @main واحد. تشكّل الكتلتان معًا البرنامج الكامل.
تعرّف الكتلة الأولى مصنّف الهندسة. وهي ترفض الأبعاد غير الصالحة صراحةً بدلًا من تحويلها بصمت إلى مستطيل مختلف. يحافظ تعداد Swift على تميّز النتائج الأربع المحتملة؛ وتوفّر قيم السلاسل الخام الرسائل المعروضة. توثيق تعداد Swift
import SwiftUI
import CoreGraphics
enum Placement: String {
case contained = "Contained"
case overlap = "Overlap"
case separate = "No area overlap"
case invalid = "Invalid geometry"
}
func isUsable(_ rect: CGRect) -> Bool {
let values = [rect.origin.x, rect.origin.y,
rect.width, rect.height,
rect.maxX, rect.maxY]
return values.allSatisfy { $0.isFinite }
&& rect.width > 0 && rect.height > 0
}
func classify(item: CGRect, target: CGRect) -> Placement {
guard isUsable(item), isUsable(target) else {
return .invalid
}
if CGRectContainsRect(target, item) {
return .contained
}
let shared = CGRectIntersection(item, target)
if !shared.isNull && shared.width > 0 && shared.height > 0 {
return .overlap
}
return .separate
}
توفّر الكتلة الثانية التطبيق ولوحة الرسم. أرفق الإيماءة بالمستطيل القابل للتحريك، كما تصف وثائق Apple الخاصة بـ DragGesture. ويُبقي الحجم الثابت للوحة هندسة هذا المثال واضحة. توثيق DragGesture
@main
struct RectanglePlacementApp: App {
var body: some Scene {
WindowGroup {
PlacementCanvas()
}
}
}
struct PlacementCanvas: View {
private let target = CGRect(x: 200, y: 100,
width: 200, height: 160)
@State private var item = CGRect(x: 40, y: 120,
width: 80, height: 60)
@State private var message = "Drag the blue item, then release."
@GestureState private var translation: CGSize = .zero
private var preview: CGRect {
item.offsetBy(dx: translation.width,
dy: translation.height)
}
private var move: some Gesture {
DragGesture(minimumDistance: 1,
coordinateSpace: .named("placementCanvas"))
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
let finalItem = item.offsetBy(
dx: value.translation.width,
dy: value.translation.height
)
let result = classify(item: finalItem, target: target)
message = result.rawValue
if result != .invalid {
item = finalItem
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(message)
ZStack(alignment: .topLeading) {
Color.white
Rectangle()
.fill(Color.gray.opacity(0.25))
.frame(width: target.width, height: target.height)
.position(x: target.midX, y: target.midY)
Rectangle()
.fill(Color.blue)
.frame(width: item.width, height: item.height)
.position(x: preview.midX, y: preview.midY)
.gesture(move)
}
.frame(width: 640, height: 380)
.coordinateSpace(.named("placementCanvas"))
Button("Reset") {
item = CGRect(x: 40, y: 120, width: 80, height: 60)
message = "Drag the blue item, then release."
}
}
.padding()
}
}
اسحب العنصر الأزرق فوق الهدف الرمادي ثم أفلتْه. تسجّل الرسالة آخر موضع اكتمل وضعه. يظل التداخل الجزئي والموضع المنفصل في مكان الإفلات؛ أما الاحتواء فلا يغيّر تلقائيًا علاقة العنصر بالأصل ولا يجعله ينطبق على حاوية أخرى. يعيد زر إعادة تعيين المستطيل إلى موضع البداية.
لا يوجد عمدًا أي تحديث دائم للموضع في استدعاء التحديث الراجع للإيماءة. تحتفظ @GestureState بترجمة المعاينة فقط، وتُعاد إلى حالتها عند خمول الإيماءة. تحدد Apple أن onEnded لا تعمل إلا بعد اكتمال ناجح، وتوصي باستخدام حالة الإيماءة للقيم التي يجب أن تُعاد بصرف النظر عن كيفية انتهاء الإيماءة. وبناءً على ذلك، يمكن للإيماءة التي تنقطع أن تتخلّص من المعاينة مع الإبقاء على المستطيل المعتمد والنتيجة السابقة. توثيق GestureState، توثيق onEnded
يتعامل هذا التنفيذ مع المستطيلات المملوءة بوصفها الهندسة. لا يؤدي إضافة ظل أو حد زخرفي إلى توسيع مستطيل القبول. وإذا أضفت تغيير الحجم أو التكبير أو التدوير، فحدّث نموذج الهندسة وتحويل الإحداثيات قبل الاعتماد على المصنّف نفسه.
افحص الحالات الحدّية قبل إضافة مزيد من العناصر
استخدم الحالات الاصطناعية التالية مع هدف المثال: نقطة الأصل (200, 100)، والعرض 200، والارتفاع 160. تصف الإحداثيات مستطيلات العناصر ضمن مساحة اللوحة نفسها.
| نقطة أصل العنصر | حجم العنصر | النتيجة المتوقعة | السبب |
|---|---|---|---|
(220, 120) |
80 × 60 |
محتوى |
تقع جميع الحواف داخل الهدف |
(360, 120) |
80 × 60 |
تداخل |
يتجاوز العنصر الحافة اليمنى للهدف |
(400, 120) |
80 × 60 |
لا يوجد تداخل مساحي |
تتلامس الحافتان العموديتان فقط |
(401, 120) |
80 × 60 |
لا يوجد تداخل مساحي |
تفصل فجوة بين المستطيلين |
(200, 100) |
200 × 160 |
محتوى |
يحقق تطابق المستطيلين شرط الاحتواء |
(180, 80) |
240 × 200 |
تداخل |
يحتوي العنصر الهدف، ما يعكس اتجاه الاحتواء |
تم التحقق من حسابات الجدول باستخدام عملية حسابية حتمية مستقلة. فُحص برنامج Swift، لكنه لم يُترجم في بيئة البحث، إذ لم يتوفر فيها مترجم Swift. أنشئه في هدف تطبيق macOS واختبر مواضع الإفلات قرب كل حد.
بالنسبة إلى عدة عناصر مخزنة في الذاكرة، احتفظ بهوية العنصر المحدد عند بدء السحب. وعند الاكتمال، قارن هندسته النهائية بالأهداف المؤهلة واستبعد العنصر نفسه. إذا انطبق الشرط على عدة أهداف، فحدّد قاعدة اختيار ثابتة، مثل ترتيب صريح للأهداف. تصنيف الهندسة وحده لا يحدد أي هدف ينبغي أن يضم العنصر.
طبّق قاعدة الهندسة نفسها في لوحة رسم AppKit
في طريقة عرض AppKit مخصصة موجودة، أبقِ مصنّف الهندسة مستقلًا عن تسليم الأحداث. يبلّغ mouseUp(with:) عن إفلات زر الفأرة الأيسر؛ استخدمه لإتمام سحب تملكه اللوحة مسبقًا فقط. يجب ألا يؤدي إفلات زر فأرة غير متعلق إلى تفعيل الوضع. توثيق إفلات زر الفأرة في AppKit
تسلسل عملي للأحداث:
- في
mouseDown(with:)، حدّد عنصرًا اصطناعيًا باستخدام قاعدة اختبار الاصطدام الحالية في اللوحة. احفظ هويته ومستطيله عند البداية وموضع المؤشر عند البداية. - حوّل كل موقع ذي صلة في حدث الفأرة إلى إحداثيات اللوحة. تعبير التحويل لطريقة عرض مرتبطة بنافذتها هو
convert(event.locationInWindow, from: nil). - في
mouseDragged(with:)، اشتق معاينة من المستطيل المحفوظ وإزاحة المؤشر. - في
mouseUp(with:)، أعد حساب الإزاحة باستخدام حدث الإفلات، وصنّف المستطيل النهائي، ثم اعتمده مرة واحدة وأزل حالة السحب النشط.
توثّق Apple أن locationInWindow هو موقع حدث الفأرة بإحداثيات النافذة. ويحوّل إصدار تحويل النقاط من NSView.convert(_:from:) الإحداثيات من إحداثيات النافذة عندما تكون وسيطة العرض هي nil. وهذا يجنب مقارنة نقطة ضمن النافذة بمستطيل معبّر عنه بإحداثيات اللوحة المحلية. توثيق موقع الحدث، توثيق تحويل النقاط
أبقِ معالجة الأحداث لدى المكوّن الذي يملك التفاعل أصلًا. تميّز Apple بين فئة فرعية مباشرة من NSView تعالج الحدث وفئة فرعية ينبغي لها أن تتيح للعنصر الأب معالجة الأحداث التي لم تستهلكها. اتبع سلوك المستجيب الموثّق؛ فإضافة منطق الوضع لا تبرر اعتراض تسلسل أحداث لطريقة عرض أخرى أو معالجة الإفلات نفسه عبر معالجين مستقلين. إرشادات معالجة الأحداث في NSView
اعرف متى لا تستطيع المستطيلات الإجابة عن سؤال يتعلق بالأشكال
يعطي المثال إجابة دقيقة للمستطيلات المملوءة والمحاذية للمحاور. أما بالنسبة إلى الدوائر أو الحدود المقعّرة أو الثقوب أو الأشكال المدورة، فلا تقدّم المستطيلات المحيطة سوى اختبار هندسي أولي. فقد تتضمن مساحتها المشتركة فراغات لا يملؤها أي من الشكلين المرئيين.
كما أن اختبار وجود مؤشر داخل مسار لا يثبت الاحتواء الكامل لشكل آخر. تقبل NSBezierPath.contains(_:) في AppKit نقطة واحدة ضمن نظام إحداثيات المسار، وتذكر Apple أنها لا تأخذ عرض الحد في الحسبان. لذلك، فإن اختبار مركز العنصر لا يجيب إلا عمّا إذا كان ذلك المركز داخل المسار. توثيق احتواء نقطة في NSBezierPath
بالنسبة إلى المسارات المملوءة في macOS 14 والإصدارات الأحدث، توثّق SwiftUI عمليات منطقية مدعومة: تنتج Path.intersection(_:eoFill:) المنطقة المملوءة المشتركة، وتنتج Path.subtracting(_:eoFill:) جزء المنطقة المملوءة الأولى الواقع خارج الثانية. توفّر هذه العمليات مسارًا قائمًا على المسارات لدراسة التداخل والجزء من العنصر الواقع خارج الهدف. حاذِ المسارين في نظام الإحداثيات نفسه واستخدم قاعدة الملء المطابقة لطريقة الرسم. توثيق تقاطع المسارات، توثيق طرح المسارات
تتعلق هذه العمليات بالمناطق المملوءة، وتفترض إغلاق المقاطع الفرعية غير المغلقة. أما الخط المرسوم بحد أو قاعدة تطبيق تتعلق بملامسة الحدود فتحتاج إلى تعريف هندسي خاص بها. أبقِ المصنّف المستطيلي للعناصر المستطيلة؛ ووسّع طبقة الهندسة عمدًا عندما تبدأ اللوحة برسم أشكال أخرى.
تنتج النتيجة الموثوقة عند الإفلات من ثلاثة خيارات ملموسة: الحفاظ على هندسة بداية السحب، وحساب العنصر النهائي ضمن نظام إحداثيات الهدف، وتطبيق سياسة واضحة للاحتواء والتداخل. ومع تثبيت هذه الخيارات، تستطيع الواجهة الإبلاغ عن النتيجة باستمرار من دون ربط الهندسة بتوجيه الأحداث أو ملكية العنصر.
المصادر
- CGRectContainsRect(::) — Apple
- CGRectIntersection(::) — Apple
- coordinateSpace(_:) — Apple
- الترجمة — Apple
- التعدادات — Swift.org
- DragGesture — Apple
- GestureState — Apple
- onEnded(_:) — Apple
- mouseUp(with:) — Apple
- locationInWindow — Apple
- convert(_:from:) — Apple
- NSView — Apple
- contains(_:) — Apple
- intersection(_:eoFill:) — Apple
- subtracting(_:eoFill:) — Apple