حافظ على استجابة رسم المستطيلات السريع عبر فصل معاينة السحب النشط عن الأشكال المثبّتة على لوحة الرسم. في تطبيق رسم لنظام Mac، اجعل سطحًا واحدًا للإيماءات مسؤولًا عن الإدخال، واحتفظ بالهندسة المؤقتة محليًا، وأضف إلى مجموعة الأشكال فقط عند انتهاء السحب بنجاح.
يستهدف هذا الدليل مطورًا يبني أداة بسيطة لرسم المستطيلات بالنقر والسحب في تطبيق macOS خاص به. تُخزَّن جميع الأشكال التوضيحية في الذاكرة. الهدف هو منع حركة المؤشر من تغيير نموذج الرسم بالكامل مرارًا، ثم استخدام التنميط لتحديد أي تكلفة متبقية في العرض.
لماذا يتباطأ الرسم مع تزايد الأشكال المثبّتة؟
ابدأ بالتمييز بين ثلاث عمليات: استقبال الإدخال، وتحديث الحالة، وعرض الهندسة. قد يغيّر السحب الهندسة المؤقتة بوتيرة متكررة. وإذا أدت هذه التغييرات أيضًا إلى استبدال المجموعة المثبّتة، أو إعادة بناء كل مسار، أو إبطال تسلسل هرمي واسع من العروض، فقد يزداد العمل المرتبط بكل حركة مع نمو الرسم.
توضح Apple أن SwiftUI يتتبع التبعيات ويحدّث العروض عند تغيّر تلك التبعيات. ويمكن أن تؤدي تحديثات العرض الفردية البطيئة والتحديثات الكثيرة غير الضرورية إلى تفويت مواعيد إطارات العرض. إن فصل حالة المعاينة التي تتغير كثيرًا عن المحتوى المثبّت هو تطبيق معماري لهذا النموذج الموثّق. تحسين أداء SwiftUI باستخدام Instruments
توفر Canvas رسمًا فوريًا عبر سياق رسومي. وتشير Apple إلى أنها قد تحسن الأداء في الرسومات الديناميكية المعقدة، لكنها لا توفر التفاعل أو إمكانية الوصول لكل عنصر مرسوم. اربط معالجة الإدخال بسطح رسم محدد بوضوح، ونفّذ دعمًا منفصلًا للتحديد وإمكانية الوصول إذا احتاج التطبيق لاحقًا إلى تشغيل الأشكال كلٌّ على حدة. Canvas
لذلك، يكون الحد الفاصل واضحًا: سطح الإدخال مسؤول عن الإيماءة، والحالة المؤقتة مسؤولة عن المعاينة، والعرض الأب مسؤول عن المستطيلات المثبّتة. يقلل تقسيم هذه المسؤوليات من التبعيات التي يمكن تجنبها. لكنه لا يضمن أن النظام لن يعيد رسم لوحة الرسم المثبّتة مطلقًا.
افصل المعاينة عن الرسم المثبّت
استخدم خطة الملكية هذه قبل اختيار تحسينات العرض:
| المسؤولية | المالك | متى تتغير |
|---|---|---|
| مستطيل السحب الحالي | سطح الإيماءة | أثناء السحب النشط |
| مجموعة المستطيلات المثبّتة | عرض الرسم الأب | مرة واحدة لكل سحب ناجح |
| رسم المستطيلات المثبّتة | لوحة الرسم الخلفية | عندما تستلزم مدخلاتها تحديثًا |
| التعرّف على المؤشر | سطح الإيماءة | طوال تسلسل الإيماءة |
للهندسة المؤقتة، تُعد @GestureState مفيدة لأن SwiftUI يعيد ضبطها عندما تصبح الإيماءة غير نشطة. ثبّت الشكل عبر onEnded، إذ تتلقى دالة الاستدعاء فيها القيمة النهائية للإيماءة. تحدد Apple أن دالة الاستدعاء هذه لا تعمل إلا عند انتهاء الإيماءة بنجاح؛ أما حالة الإيماءة فتتكفل بالتنظيف بصرف النظر عن كيفية انتهائها. GestureState، onEnded(_:)
لا تتطلب الهندسة النهائية للمستطيل سوى موضعي البداية والموضع الحالي. لا حاجة إلى الاحتفاظ بكل موضع وسيط للمؤشر. توثّق Apple هذه المواضع بشكل منفصل عن نقاط النهاية المتوقعة؛ استخدم الموضع النهائي الفعلي عند تثبيت المستطيل. DragGesture.Value
أنشئ أداة مستطيلات كاملة تعمل في الذاكرة
يشكّل مقطعا الشيفرة التاليان ملف Swift كاملًا. في مشروع تطبيق SwiftUI لنظام macOS يستهدف macOS 12 أو أحدث، استبدل ملف إدخال التطبيق المُنشأ بالمقطعين واحذف أي تعريف آخر لنقطة دخول التطبيق. تتوفر Canvas بدءًا من macOS 12. تمت مراجعة هذا المثال، لكن تعذّر تجميعه هنا لأن البيئة لا تحتوي على سلسلة أدوات Swift. Canvas
يعرّف المقطع الأول التطبيق، والمجموعة المثبّتة، وعارضها. لا يتلقى العرض الأب سوى المستطيلات المكتملة؛ ولا يقرأ حالة السحب النشط.
import SwiftUI
@main
struct RectangleDrawingApp: App {
var body: some Scene {
WindowGroup {
DrawingBoard()
}
}
}
struct DrawingBoard: View {
@State private var rectangles: [CGRect] = []
var body: some View {
VStack(spacing: 8) {
Text("Drag to draw a rectangle")
Text("Committed rectangles: \(rectangles.count)")
ZStack {
Color.white
CommittedRectangles(rectangles: rectangles)
.allowsHitTesting(false)
RectangleGestureSurface { rectangle in
rectangles.append(rectangle)
}
}
.clipped()
.frame(minWidth: 480, minHeight: 320)
}
.padding()
}
}
struct CommittedRectangles: View {
let rectangles: [CGRect]
var body: some View {
Canvas { context, _ in
for rectangle in rectangles {
context.stroke(
Path(rectangle),
with: .color(.blue),
lineWidth: 2
)
}
}
}
}
أضف المقطع التالي إلى الملف نفسه. يغطي السطح الشفاف منطقة الرسم، ويمنح اختبار استهداف اللمس شكلًا مستطيلًا صريحًا، ويتولى إيماءة الرسم الوحيدة. توثّق Apple أن contentShape يحدد الشكل المستخدم لاختبار استهداف اللمس، وأن allowsHitTesting(false) يستثني العرض من اختبار استهداف اللمس. contentShape(_:eoFill:)، allowsHitTesting(_:)
struct RectangleGestureSurface: View {
let commit: (CGRect) -> Void
@GestureState private var preview: CGRect? = nil
var body: some View {
ZStack {
Color.clear
Canvas { context, _ in
if let rectangle = preview {
context.stroke(
Path(rectangle),
with: .color(.orange),
lineWidth: 2
)
}
}
.allowsHitTesting(false)
}
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.updating($preview) { value, state, _ in
state = Self.rectangle(
from: value.startLocation,
to: value.location
)
}
.onEnded { value in
let rectangle = Self.rectangle(
from: value.startLocation,
to: value.location
)
commit(rectangle)
}
)
}
private static func rectangle(
from start: CGPoint,
to end: CGPoint
) -> CGRect {
CGRect(
x: min(start.x, end.x),
y: min(start.y, end.y),
width: abs(end.x - start.x),
height: abs(end.y - start.y)
)
}
}
تزيل minimumDistance: 0 شرط الحد الأدنى للمسافة اللازمة للتعرّف على الإيماءة. وتحافظ مساحة الإحداثيات المحلية على محاذاة مواضع الإيماءة مع لوحة المعاينة ذات الحجم المطابق. هذه خيارات موثّقة لإعداد الإيماءات، وليست ضمانًا بشأن وتيرة الأحداث أو زمن تأخر العرض. DragGesture
تطبّع الدالة المساعدة للمستطيل اتجاهات السحب الأربعة. على سبيل المثال، ينتج عن السحب من (90, 80) إلى (30, 20) أصلٌ عند (30, 20) وحجمٌ قدره (60, 60). تستخدم المعاينة والتثبيت الحساب نفسه، لذا لا يؤدي إفلات المؤشر إلى تغيير معادلة الهندسة.
تثبّت هذه الأداة البسيطة المستطيلات ذات العرض الصفري والارتفاع الصفري أيضًا. لذلك قد تؤدي النقرة إلى زيادة العدد من دون إنشاء مستطيل مرئي. إذا كان المنتج يتطلب حدًا أدنى للحجم، فأضف هذه القاعدة صراحةً عند التثبيت وقدّم للمستخدم ملاحظات مناسبة. يحتفظ المثال بكل إيماءة مكتملة بنجاح بدلًا من تصفية النتائج الصغيرة بصمت.
تُقص المنطقة المرئية، بينما تبقى الهندسة المخزّنة دون تغيير. لذلك قد يحتوي المستطيل الممتد خارج اللوحة على إحداثيات خارجها. حدّد ما إذا كان تغيير الحجم مستقبلًا ينبغي أن يُظهر تلك الهندسة قبل إضافة تقييد للإحداثيات.
شخّص التكلفة المتبقية قبل إضافة ذاكرة تخزين مؤقت
يزيل المثال تعديلات النموذج المثبّت من مسار تحديث السحب. لكن العارض المثبّت ما زال يمر على المجموعة كلما نُفّذ ذلك العارض. ميّز بين تقليل انتشار تغييرات الحالة وتقليل عمل العرض: فوجود عرض منفصل لا يعني وجود ذاكرة تخزين مؤقت دائمة للصورة وفقًا للتوثيق.
استخدم مقارنة مضبوطة لمعرفة التكلفة المتبقية. اختر مجموعات توضيحية في الذاكرة مثل 0 و500 و5,000 مستطيل. وحافظ على ثبات أبعاد المستطيلات وحجم النافذة ومقياس الشاشة وإعدادات البناء. هذه الأعداد مدخلات للاختبار، وليست حدودًا للسعة أو نتائج قياس.
سجّل ثلاث فترات منفصلة: السحب مع بقاء المجموعة دون تغيير، والإفلات لتثبيت الشكل، وتغيير حجم النافذة. يشير التباطؤ المستمر أثناء السحب إلى عمل يختلف عن التوقف الذي يحدث عند التثبيت فقط. وقد يكشف سلوك تغيير الحجم عن إعادة بناء الهندسة أو ذاكرة التخزين المؤقت، وهو ما قد لا يظهر أثناء الرسم العادي.
تصف جلسة Apple حول تنميط SwiftUI أدوات Long View Body Updates وLong Representable Updates وCause & Effect Graph إلى جانب Time Profiler. استخدم مخطط التبعيات للتحقق من سبب تحديث العروض المثبّتة أثناء السحب، واستخدم عينات وحدة المعالجة المركزية للعثور على عمليات إنشاء الهندسة أو العرض المكلفة. قدّمت Apple سير العمل هذا في Instruments 26. تحسين أداء SwiftUI باستخدام Instruments
عند حدوث توقفات أوسع، تبدأ جلسة WWDC26 من Apple حول الاستجابة باستخدام Time Profiler، وتميّز بين التنفيذ المشغول والوقت المستغرق في الانتظار. طبّق هذا التمييز قبل نقل العمل بين المنفّذات أو استبدال مجموعة العرض. أنشئ ملفًا للأداء، وأصلح المشكلات، وتحقق من التحسين: حسّن استجابة التطبيق باستخدام Instruments
أداة اتخاذ القرار التالية استنتاج هندسي مستمد من أدوات التنميط هذه وتصميم الملكية أعلاه:
| السلوك الملحوظ | ما ينبغي التحقق منه أولًا | التغيير المقترح |
|---|---|---|
| تتغير حالة الرسم الأب أثناء حركة المؤشر | دوال استدعاء تحديث الإيماءة والحالة المشتركة | أبقِ حالة المعاينة داخل سطح الإيماءة |
| يهيمن رسم المحتوى المثبّت على العينات أثناء الحركة | تكرار المرور على العناصر وإنشاء المسارات | احسب الهندسة مسبقًا أو أضف ذاكرة تخزين مؤقت للمحتوى المثبّت |
| تطول فترات التوقف عند الإفلات مع نمو المجموعة | تعديل المصفوفة وإعادة بناء الرسم بالكامل | قلل اللقطات المحتفظ بها وأعد بناء المحتوى المشتق المتأثر فقط |
| يستجيب رسم المستطيلات بسرعة، لكن تبطؤ رسم المسارات الحرة الطويلة | حجم المسار النشط وإعادة بنائه | حلّل المسار النشط المتنامي بصورة منفصلة |
| تحدث التوقفات أساسًا عند تغيير الحجم | العرض المعتمد على الحجم وإبطال ذاكرة التخزين المؤقت | حدّد سلوكًا صريحًا لتغيير الحجم للمحتوى المشتق |
تضيف ذاكرة التخزين المؤقت حدًا آخر للملكية: تظل هندسة المستطيلات هي المرجع الأساسي، بينما تبقى ذاكرة التخزين المؤقت محتوى مشتقًا يمكن استبداله. أدرج حجم الرسم ومقياس الشاشة والتنسيق ذي الصلة في تصميم الإبطال. في الرسم الذي تقتصر عملياته على الإضافة، تُعد تحديثات ذاكرة التخزين المؤقت التدريجية تحسينًا محتملًا؛ أما التعديلات والحذف فتتطلب استراتيجية واضحة لإعادة البناء أو الإبطال على مستوى المناطق. تحقّق من النتيجة باستخدام مجموعات الإدخال نفسها قبل الإبقاء على التعقيد الإضافي.
تحقّق من نسخ المصفوفات عند حد التثبيت
تتمتع مصفوفات Swift بدلالات القيم وتستخدم التخزين بالنسخ عند التعديل. ويمكن للنسخ مشاركة التخزين إلى أن يتطلب التعديل تخزينًا مستقلًا؛ أما المصفوفات التي تحتوي على مثيلات أصناف فتظل تشارك المراجع إلى تلك المثيلات. لا ينبغي وصف تمرير مصفوفة إلى عرض أو إضافة عنصر إليها بأنه ينسخ كل العناصر دائمًا. Array
تجنّب تخزين لقطة جديدة كاملة للرسم عند كل تحديث للمؤشر في هذه الأداة. إذا كشف التنميط عن نسخ أثناء التثبيت، فتحقّق من اللقطات التي ما زالت حية وكيفية مشاركة نموذج الرسم. إن استبدال أنواع القيم بكائنات قابلة للتعديل ومشتركة لمجرد تجنب النسخ يغيّر دلالات الملكية ويتطلب قرارًا تصميميًا منفصلًا.
تعامل مع العرض غير المتزامن بوصفه خيارًا يخضع للقياس
تعرّف Apple rendersAsynchronously بأنها تسمح للوحة الرسم بعرض محتوياتها على العرض الأب بصورة غير متزامنة. لا يَعِد هذا الوصف بتسريع وصول إيماءات السحب، ولا ينقل معالج الإيماءة إلى منفّذ في الخلفية، ولا يلغي المرور على المجموعة. قيّمه على حدة بعد تحديد حدود الإدخال والحالة. rendersAsynchronously
متى ينبغي أن تتولى AppKit سطح الرسم؟
استخدم سطح AppKit مخصصًا عندما تتطلب المهمة معالجة مباشرة لأساليب المستجيب أو إبطالًا صريحًا لمناطق الرسم. توثّق Apple أن NSView توفر بنية الرسم ومعالجة الأحداث، وتدعم إنشاء فئات فرعية لسلوك مخصص. وهذا بديل مدعوم له مسؤولياته الخاصة في العرض. NSView
في تنفيذ AppKit، حافظ على التسلسل نفسه: ابدأ الهندسة المؤقتة في mouseDown(with:)، وحدّثها في mouseDragged(with:)، وثبّتها في mouseUp(with:). حوّل event.locationInWindow إلى إحداثيات العرض المستقبِل قبل حساب الهندسة. يوثّق دليل Apple المؤرشف لأحداث الفأرة هذا التسلسل وتحويل الإحداثيات؛ أما المرجع الحالي لـ NSView فيوثّق أساليب المستجيب المدعومة. معالجة أحداث الفأرة، NSView
اطلب إعادة العرض بتعيين needsDisplay = true؛ وتحدد Apple أن العرض يُعاد عرضه عند دورة حلقة الأحداث التالية. في تصميم يعتمد على المناطق، أبطِل اتحاد حدود المعاينة القديمة والجديدة، مع توسيعه ليشمل الحد. لا يقلل الإبطال الإقليمي المرور على العناصر إلا إذا تجنب تنفيذ الرسم أيضًا معالجة المحتوى غير المتأثر. needsDisplay، معالجة أحداث الفأرة
أدرج السطح باستخدام NSViewRepresentable: أنشئه في makeNSView، ووفّق الحالة الخارجية في updateNSView، واستخدم منسقًا لإبلاغ SwiftUI بالتغييرات. حدّث مسار دوال الاستدعاء أثناء عمليات التحديث، وتجنّب إعادة ضبط المسودة النشطة لمجرد أن SwiftUI وفّرت قيمة أخرى للعنصر القابل للتمثيل. وتذكر Apple أيضًا أن SwiftUI تتحكم في تخطيط العرض الممثَّل؛ فلا تستبدل مباشرةً الإطار أو الحدود التي تديرها. NSViewRepresentable
اختر مالكًا واحدًا لإدخال الرسم. إذا تولت AppKit تسلسل الرسم، فلا تثبّت التسلسل نفسه أيضًا عبر إيماءة سحب متداخلة في SwiftUI. وبالنسبة إلى فئة فرعية مباشرة من NSView، توجّه Apple التطبيقات التي تعالج أحداث الفأرة المحددة إلى عدم استدعاء تنفيذ الفئة الأب لتلك الأحداث التي تعالجها. NSView
اجعل التحسين الأول صغيرًا وسهل الملاحظة: حالة معاينة محلية، وحد تثبيت واحد، وعارض للمحتوى المثبّت لا تتغير مدخلاته أثناء الحركة. ثم قارن رسومات اصطناعية متزايدة الحجم في Instruments. يوضح لك التتبّع الناتج ما إذا كان التغيير المفيد التالي يتعلق بإدارة التبعيات أو ملكية المجموعة أو إعداد الهندسة أو العرض.
المصادر
Canvas — وثائق Apple للمطورين.
GestureState — وثائق Apple للمطورين.
onEnded(_:) — وثائق Apple للمطورين.
DragGesture — وثائق Apple للمطورين.
DragGesture.Value — وثائق Apple للمطورين.
contentShape(_:eoFill:) — وثائق Apple للمطورين.
allowsHitTesting(_:) — وثائق Apple للمطورين.
Array — وثائق Apple للمطورين.
rendersAsynchronously — وثائق Apple للمطورين.
تحسين أداء SwiftUI باستخدام Instruments — Apple للمطورين.
أنشئ ملفًا للأداء، وأصلح المشكلات، وتحقق من التحسين: حسّن استجابة التطبيق باستخدام Instruments — Apple للمطورين.
NSView — وثائق Apple للمطورين.
needsDisplay — وثائق Apple للمطورين.
NSViewRepresentable — وثائق Apple للمطورين.
معالجة أحداث الفأرة — أرشيف وثائق Apple.