تحسين أداء SwiftUI للقوائم الطويلة: حلول عملية
تحسين أداء SwiftUI للقوائم الطويلة: حلول عملية لإعادة العرض، هوية الصف الثابتة، الترقيم، تحميل الصور، وتمرير سلس على أجهزة iPhone الأقدم.

كيف تظهر "القوائم البطيئة" في تطبيقات SwiftUI الحقيقية
القائمة "البطيئة" في SwiftUI عادةً ليست خطأ. هي اللحظة التي لا تستطيع فيها الواجهة مواكبة إصبعك. تلاحظ ذلك أثناء التمرير: تتوقف القائمة لحظة، تسقط الإطارات، ويشعر كل شيء بثقل.
العلامات النموذجية:
- التمرير متقطّع، خاصةً على الأجهزة الأقدم
- الصفوف تومض أو تعرض محتوى خاطئًا لفترة قصيرة
- النقرات تتأخر، أو تبدأ إجراءات السحب متأخرة
- الهاتف يسخن وتستنزف البطارية أسرع من المتوقع
- استخدام الذاكرة ينمو كلما طالت مدة التمرير
يمكن أن تبدو القوائم الطويلة بطيئة حتى لو كان كل صف يبدو "صغيرًا"، لأن التكلفة ليست مجرد رسم البيكسلات. SwiftUI لا يزال بحاجة لمعرفة هوية كل صف، حساب التخطيط، حلّ الخطوط والصور، تشغيل كود التنسيق الخاص بك، ومقارنة التغيرات عند تحديث البيانات. إذا حدث أي من هذه الأعمال كثيرًا، تصبح القائمة نقطة حرارة.
من المفيد أيضًا فصل فكرتين. في SwiftUI، "إعادة العرض" غالبًا ما تعني إعادة حساب body الخاص بالعرض. هذا الجزء عادةً رخيص. العمل المكلف هو ما يؤدي إليه هذا الحساب: تخطيط ثقيل، فك ترميز الصور، قياس النص، أو إعادة بناء صفوف كثيرة لأن SwiftUI تعتقد أن هوياتها تغيرت.
تخيل دردشة بها 2000 رسالة. تصل رسائل جديدة كل ثانية، وكل صف ينسق الطوابع الزمنية، يقيس نصًا متعدد الأسطر، ويحمّل الصور الرمزية. حتى لو أضفت عنصرًا واحدًا، يمكن أن يتسبب تغيير حالة ذو نطاق واسع في إعادة تقييم الكثير من الصفوف، وبعضها قد يُعاد رسمه.
الهدف ليس تحسين دقيق فقط. تريد تمريرًا سلسًا، نقرات فورية، وتحديثات تلامس فقط الصفوف التي تغيرت فعلاً. تركز الإصلاحات أدناه على هوية مستقرة، صفوف أرخص، تحديثات غير ضرورية أقل، وتحميل متحكم به.
الأسباب الرئيسية: الهوية، العمل لكل صف، وعواصف التحديث
عندما تشعر أن قائمة SwiftUI بطيئة، نادرًا ما تكون المشكلة "عدد الصفوف الكبير". بل هو العمل الإضافي الذي يحدث أثناء التمرير: إعادة بناء الصفوف، إعادة حساب التخطيط، أو إعادة تحميل الصور مرارًا.
تقع غالبية الأسباب في ثلاثة خزانات:
- هوية غير مستقرة: الصفوف لا تملك
idثابتًا، أو تستخدم\\.selfلقيم يمكن أن تتغير. لا يستطيع SwiftUI مطابقة الصفوف القديمة بالجديدة، لذا يعيد بناء أكثر مما يلزم. - عمل كبير لكل صف: تنسيق تواريخ، ترشيح، تغيير حجم الصور، أو القيام بعمل شبكة/قرص داخل عرض الصف.
- عواصف التحديث: تغيير واحد (كتابة نص، نبضة مؤقت، تحديث تقدم) يطلق تحديثات حالة متكررة، فتتجدد القائمة مرارًا.
مثال: لديك 2000 طلب. كل صف ينسق العملة، يبني سلسلة منسقة، ويبدأ جلب صورة. وفي نفس الوقت، مؤقت "آخر تزامن" يحدث مرة في الثانية في العرض الأعلى. حتى لو لم تتغير بيانات الطلبات، قد يبطل ذلك القائمة بما يكفي لجعل التمرير متقطعًا.
لماذا List و LazyVStack قد يشعران بأنهما مختلفان
List أكثر من مجرد ScrollView. صُمّم حول سلوك الجدول/المجموعة وتحسينات النظام. غالبًا ما يتعامل مع مجموعات بيانات كبيرة بذاكرة أقل، لكنه قد يكون حساسًا للهوية والتحديثات المتكررة.
ScrollView + LazyVStack يعطيك تحكمًا أكبر في التخطيط والمظهر، لكنه أيضًا أسهل أن تتسبب عن غير قصد في عمل تخطيط إضافي أو إطلاق تحديثات مكلفة. على الأجهزة الأقدم، يظهر هذا العمل الإضافي أسرع.
قبل إعادة كتابة واجهتك، قِس أولًا. إصلاحات صغيرة مثل معرفات مستقرة، نقل العمل خارج الصفوف، وتقليل تغير الحالة غالبًا ما تحل المشكلة دون تغيير الحاويات.
إصلاح هوية الصف حتى يتمكن SwiftUI من المقارنة بكفاءة
عندما تشعر بأن القائمة الطويلة متقطعة، غالبًا ما تكون الهوية هي السبب. يقرر SwiftUI أي الصفوف يمكن إعادة استخدامها بمقارنة المعرفات. إذا تغيّرت تلك المعرفات، يعتبر SwiftUI الصفوف جديدة، يتخلص من القديمة، ويعيد بناء أكثر مما يلزم. قد يظهر ذلك كإعادة عرض عشوائية، فقدان موضع التمرير، أو تشغيل رسوم متحركة دون سبب.
الفوز الأبسط: اجعل id كل صف ثابتًا ومرتبطًا بمصدر بياناتك.
خطأ شائع هو توليد الهوية داخل العرض:
ForEach(items) { item in
Row(item: item)
.id(UUID())
}
هذا يُجبر على معرف جديد في كل عرض، لذا يصبح كل صف "مختلفًا" في كل مرة.
فضل المعرفات الموجودة بالفعل في النموذج، مثل مفتاح قاعدة البيانات، معرف الخادم، أو سِلَج ثابت. إذا لم يكن لديك واحد، أنشئه مرة عند إنشاء النموذج - لا تنشئه داخل العرض.
struct Item: Identifiable {
let id: Int
let title: String
}
List(items) { item in
Row(item: item)
}
احذر من المؤشرات. ForEach(items.indices, id: \\.self) يربط الهوية بالموقع. إذا أدخلت أو حذفت أو رتبت، "تتحرك" الصفوف، وقد يعيد SwiftUI استخدام العرض الخطأ للبيانات الخطأ. استخدم المؤشرات فقط للمصفوفات الثابتة حقًا.
إذا استخدمت id: \\.self فتأكد أن قيمة الـ Hashable ثابتة مع مرور الوقت. إذا تغير الهاش عندما يتغير حقل، تتغير هوية الصف أيضًا. قاعدة آمنة لـ Equatable وHashable: اعتمدهما على معرف واحد وثابت، لا على خصائص قابلة للتحرير مثل name أو isSelected.
فحوصات سلامة:
- المعرفات تأتي من مصدر البيانات (ليس
UUID()في العرض) - المعرفات لا تتغير عندما يتغير محتوى الصف
- الهوية لا تعتمد على موضع المصفوفة ما لم تكن القائمة لا تعيد ترتيب نفسها
قلّل من إعادة العرض بجعل عروض الصف أرخص
غالبًا ما تبدو القائمة الطويلة بطيئة لأن كل صف يقوم بالكثير من العمل في كل مرة يُعاد فيها تقييم body الخاص به. الهدف بسيط: اجعل كل صف رخيصًا لإعادة البناء.
تكلفة مخفية شائعة هي تمرير قيم "كبيرة" إلى الصف. هياكل كبيرة، نماذج متداخلة عميقة، أو خصائص محسوبة ثقيلة يمكن أن تُشغّل عملًا إضافيًا حتى عندما تبدو الواجهة دون تغيير. قد تعيد بناء سلاسل، تحليل تواريخ، تغيير حجم صور، أو إنتاج أشجار تخطيط معقدة أكثر مما تظن.
انقل الأعمال المكلفة خارج body
إذا كان شيء ما بطيئًا، فلا تعيد بنائه داخل body للصف مرارًا. قم بحسابه مسبقًا عند وصول البيانات، احفظه في ViewModel، أو استخدم تذكرة حفظ (memoize) في مساعد صغير.
تكاليف على مستوى الصف تتجمع سريعًا:
- إنشاء
DateFormatterأوNumberFormatterجديد لكل صف - تنسيق سلاسل ثقيل في
body(انضمامات، تعابير نمطية، تحليل markdown) - بناء مصفوفات مشتقة باستخدام
.mapأو.filterداخلbody - قراءة كتل كبيرة وتحويلها (مثل فك تشفير JSON) في العرض
- تخطيط معقد جدًا مع ستاكات متداخلة وشروط متعددة
مثال بسيط: اجعل المحولات ثابتة، ومرّر سلاسل معدّة مسبقًا إلى الصف.
enum Formatters {
static let shortDate: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
f.timeStyle = .none
return f
}()
}
struct OrderRow: View {
let title: String
let dateText: String
var body: some View {
HStack {
Text(title)
Spacer()
Text(dateText).foregroundStyle(.secondary)
}
}
}
قسّم الصفوف واستخدم Equatable عندما يناسب
إذا كان جزء صغير فقط يتغير (مثل عداد)، عزله إلى مكون فرعي حتى يبقى بقية الصف ثابتًا. بالنسبة لواجهة مدفوعة بالقيمة، جعل مكون فرعي Equatable (أو لفّه بـ EquatableView) يمكن أن يساعد SwiftUI في تخطي العمل عندما لم تتغير المدخلات. احتفظ بمدخلات الـ equatable صغيرة ومحددة - لا تجعلها النموذج بأكمله.
تحكم في تحديثات الحالة التي تطلق تحديث كامل للقائمة
أحيانًا الصفوف تكون جيدة، لكن هناك شيء يستمر في إعلام SwiftUI بتحديث القائمة بأكملها. أثناء التمرير، حتى التحديثات الصغيرة الإضافية قد تتحول إلى تقطعات، خاصة على الأجهزة الأقدم.
سبب شائع هو إعادة إنشاء نموذجك كثيرًا. إذا أعاد العرض الأعلى إنشائه واستخدمت @ObservedObject لنموذج العرض الذي يملكه العرض، فقد يعيد SwiftUI إنشاؤه، يعيد الاشتراكات، ويطلق نشرات جديدة. إذا كان العرض هو المالك، استخدم @StateObject حتى يُنشأ مرة واحدة ويظل ثابتًا. استخدم @ObservedObject للكائنات المحقونة من الخارج.
قاتل أداء هادئ آخر هو النشر المتكرر جدًا. المؤقتات، أنابيب Combine، وتحديثات التقدم قد تطلق مرات كثيرة في الثانية. إذا كانت خاصية منشورة تؤثر على القائمة (أو موجودة على ObservableObject مشترك يستخدمه الشاشة)، فكل نبضة قد تبطل القائمة.
مثال: لديك حقل بحث يحدث query في كل ضغطة مفتاح، ثم يفلتر 5000 عنصر. إذا قمت بالتصفية فورًا، ستقوم القائمة بإعادة المقارنة باستمرار أثناء الكتابة. قم بعمل debounce للاستعلام، وحدث المصفوفة المفلترة بعد توقف قصير.
أنماط تساعد عادةً:
- أبقِ القيم سريعة التغير خارج الكائن الذي يقود القائمة (استخدم كائنات أصغر أو
@Stateمحلي) - نفّذ نزع تذبذب للبحث والترشيح حتى تتحدث القائمة بعد توقف الكتابة
- تجنّب نشرات مؤقت عالية التردد؛ حدّث أقل أو فقط عندما تتغير القيمة فعلاً
- احتفظ بحالة كل صف محليًا (مثل
@Stateفي الصف) بدلًا من قيمة عامة تتغير باستمرار - قسّم النماذج الكبيرة:
ObservableObjectواحد لبيانات القائمة، وآخر لحالة واجهة الشاشة
الفكرة بسيطة: اجعل وقت التمرير هادئًا. إذا لم يتغير شيء مهم، يجب ألا يُطلب من القائمة القيام بعمل.
اختر الحاوية المناسبة: List مقابل LazyVStack
الحاوية التي تختارها تؤثر على مقدار العمل الذي يقوم به نظام iOS نيابة عنك.
List عادةً الخيار الآمن عندما تبدو واجهتك كجدول قياسي: صفوف مع نصوص، صور، إجراءات سحب، اختيار، فواصل، وضع التحرير، وإمكانيات الوصول. تحت السطح، يستفيد من تحسينات النظام التي صقّلتها Apple لسنوات.
ScrollView مع LazyVStack ممتاز عندما تحتاج تخطيطًا مخصصًا: بطاقات، كتل محتوى مختلطة، رؤوس خاصة، أو تصميم على غرار الخلاصة. "الكسل" يعني أنه يبني الصفوف عند ظهورها على الشاشة، لكنه لا يمنحك نفس سلوك List في كل الحالات. مع مجموعات بيانات كبيرة جدًا، قد يعني ذلك استخدام ذاكرة أعلى وتمرير أكثر تلعثمًا على الأجهزة الأقدم.
قاعدة قرار بسيطة:
- استخدم
Listللشاشات الجدولية التقليدية: الإعدادات، الصناديق الواردة، الطلبات، قوائم الإدارة - استخدم
ScrollView+LazyVStackللتخطيطات المخصصة والمحتوى المختلط - إذا كان لديك آلاف العناصر وتحتاج فقط جدولًا، ابدأ بـ
List - إذا كنت تحتاج سيطرة بكسل-دقيقة، جرّب
LazyVStackثم قس الذاكرة وسقوط الإطارات
راقب أيضًا الأنماط التي تُبطئ التمرير تدريجيًا. تأثيرات على كل صف مثل الظل، الطمس، والتراكبات المعقدة قد تجبر على عمل رسم إضافي. إذا أردت عمقًا، طبّق التأثيرات الثقيلة على عناصر صغيرة (مثل أيقونة) بدلًا من الصف بأكمله.
مثال ملموس: شاشة "الطلبات" مع 5000 صف غالبًا تظل سلسة في List لأن الصفوف تُعاد استخدامها. إذا حولت إلى LazyVStack وبنيت صفوفًا على شكل بطاقات بظلال كبيرة وتراكبات متعددة، قد ترى تلعثمًا رغم أن الكود يبدو نظيفًا.
ترقيم الصفحات يشعر بالسلاسة ويتفادى ارتفاع الذاكرة
الترقيم يحافظ على سرعة القوائم الطويلة لأنك تعرض عددًا أقل من الصفوف، تحتفظ بعدد أقل من النماذج في الذاكرة، وتمنح SwiftUI عمل مقارنة أقل.
ابدأ بعقد ترقيم واضح: حجم صفحة ثابت (مثلاً 30 إلى 60 عنصرًا)، علم "لا مزيد من النتائج"، وصف تحميل يظهر فقط أثناء الجلب.
فخ شائع هو إطلاق الصفحة التالية فقط عند ظهور الصف الأخير تمامًا. هذا غالبًا متأخر جدًا، فيرى المستخدم توقفًا. بدلاً من ذلك، ابدأ التحميل عندما يظهر أحد الصفوف القليلة الأخيرة.
هذا نمط بسيط:
@State private var items: [Item] = []
@State private var isLoading = false
@State private var reachedEnd = false
func loadNextPageIfNeeded(currentIndex: Int) {
guard !isLoading, !reachedEnd else { return }
let threshold = max(items.count - 5, 0)
guard currentIndex >= threshold else { return }
isLoading = true
Task {
let page = try await api.fetchPage(after: items.last?.id)
await MainActor.run {
let newUnique = page.filter { p in !items.contains(where: { $0.id == p.id }) }
items.append(contentsOf: newUnique)
reachedEnd = page.isEmpty
isLoading = false
}
}
}
هذا يتجنب مشكلات شائعة مثل الصفوف المكررة (نتائج API متداخلة)، حالات التسابق من مكالمات onAppear متعددة، وتحميل الكثير دفعة واحدة.
إذا كانت قائمتك تدعم السحب للتحديث، أعد حالة الترقيم بعناية (افرغ العناصر، أعد reachedEnd، وألغِ المهام الجارية إن أمكن). إذا تحكمت في الباك أند، تجعل المعرفات الثابتة والترقيم القائم على المؤشر واجهة المستخدم أكثر سلاسة بشكل ملحوظ.
الصور والنص والتخطيط: اجعل رسم الصف خفيفًا
نادراً ما تشعر القائمة الطويلة ببطيء بسبب الحاوية نفسها. في معظم الأحيان، المشكلة في الصف. الصور هي الجاني المعتاد: فك الترميز، تغيير الحجم، والرسم قد يفوق سرعة التمرير لديك، خاصة على الأجهزة الأقدم.
إذا كنت تحمل صورًا عن بُعد، تأكد أن العمل الثقيل لا يحدث على الخيط الرئيسي خلال التمرير. أيضًا تجنب تنزيل أصول عالية الدقة لملفات مصغرة بحجم 44-80 نقطة.
مثال: شاشة "الرسائل" مع صور رمزية. إذا حمّل كل صف صورة بحجم 2000x2000، ويقيّمها، ويطبّق تشويشًا أو ظلًا، ستتلعثم القائمة حتى لو كان نموذج البيانات بسيطًا.
اجعل عمل الصور متوقعًا
عادات ذات تأثير كبير:
- استخدم صور مصغرة مولّدة من الخادم مقاربة للحجم المعروض
- فك وترتيب الصور خارج الخيط الرئيسي حيث أمكن
- خزّن المصغرات حتى لا يعيد التمرير السريع التحميل أو فك الترميز
- استخدم عنصرًا نائبًا يطابق الحجم النهائي لتجنب الوميض وقفز التخطيط
- تجنّب المُعدّلات المكلفة على الصور في الصفوف (ظلال ثقيلة، أقنعة، طمس)
ثبّت التخطيط لتجنب الارتداد
قد يقضي SwiftUI وقتًا أكثر في القياس منه في الرسم إذا استمر ارتفاع الصف بالتغير. حاول جعل الصفوف متوقعة: إطارات ثابتة للصور المصغرة، حدود لأسطر النص، وتباعد ثابت. إذا كان النص يمكن أن يتوسع، حدده (مثلاً سطر واحد إلى سطرين) حتى لا يجبر تحديث واحد على عمليات قياس إضافية.
البدائل مهمة أيضًا. دائرة رمادية تتحول لاحقًا إلى صورة رمزية يجب أن تشغل نفس الإطار، حتى لا يعاد تدفق الصف أثناء التمرير.
كيف تقيس: فحوصات Instruments تكشف عن عنق الزجاجة الحقيقي
العمل على الأداء تخمين إذا اعتمدت على الشعور فقط. Instruments يخبرك بما يعمل على الخيط الرئيسي، ما الذي يُنشأ أثناء التمرير السريع، وما الذي يسبب فقدان الإطارات.
حدد خط أساس على جهاز حقيقي (يفضل أقدمه إن كنت تدعمه). قم بعمل إجراء قابل للتكرار: افتح الشاشة، مرّر من الأعلى إلى الأسفل بسرعة، أطلق تحميل المزيد مرة، ثم عد إلى الأعلى. لاحظ أسوأ نقاط التلعثم، ذروة الذاكرة، وما إذا ظلت الواجهة متجاوبة.
ثلاث شاشات في Instruments تستحق الاهتمام
استخدمها معًا:
- Time Profiler: ابحث عن ذروات الخيط الرئيسي أثناء التمرير. التخطيط، قياس النص، فك ترميز JSON، وترميز الصور هنا تشرح غالبًا التلعثم.
- Allocations: راقب ارتفاع الكائنات المؤقتة أثناء التمرير السريع. هذا غالبًا يشير لتنسيق متكرر، سلاسل منسقة جديدة، أو إعادة بناء نماذج على مستوى الصف.
- Core Animation: تأكد من فقدان الإطارات وأوقات الإطار الطويلة. هذا يساعد على فصل ضغط العرض عن العمل البطيء على البيانات.
عندما تجد ذروة، انقر في شجرة الاستدعاءات واسأل: هل هذا يحدث مرة واحدة لكل شاشة، أم مرة لكل صف، لكل تمريرة؟ الثانية هي ما يكسر السلاسة.
أضف إشارات (signposts) لأحداث التمرير والتحميل
العديد من التطبيقات تقوم بعمل إضافي أثناء التمرير (تحميل الصور، الترقيم، الترشيح). الإشارات تساعدك على رؤية تلك اللحظات في المخطط الزمني.
import os
let log = OSLog(subsystem: "com.yourapp", category: "list")
os_signpost(.begin, log: log, name: "LoadMore")
// fetch next page
os_signpost(.end, log: log, name: "LoadMore")
أعد الاختبار بعد كل تغيير، واحدًا تلو الآخر. إذا تحسّن FPS لكن Allocations ازدادت، فقد تكون استبدلت التلعثم بضغط الذاكرة. احتفظ بملاحظات الخط الأساسي ولا تُبقَ إلا التغييرات التي تحسّن الأرقام بشكل صحيح.
أخطاء شائعة تقتل أداء القائمة بهدوء
بعض المشكلات واضحة (صور كبيرة، مجموعات بيانات ضخمة). أخرى تظهر فقط عندما تنمو البيانات، خاصة على الأجهزة الأقدم.
1) معرفات الصف غير المستقرة
خطأ كلاسيكي هو إنشاء معرفات داخل العرض، مثل id: \\.self لأنواع مرجعية، أو UUID() في جسم الصف. يستخدم SwiftUI الهوية لمقارنة التحديثات. إذا تغير المعرف، يعامل SwiftUI الصف جديدًا، يعيد بنائه، وقد يتخلص من تخطيط مخبأ.
استخدم معرفًا ثابتًا من النموذج (مفتاح قاعدة البيانات، معرف الخادم، أو UUID مخزن تم إنشاؤه مرة عند إنشاء العنصر). إذا لم يكن لديك واحد، أضفه.
2) عمل ثقيل داخل onAppear
onAppear يعمل أكثر مما يتوقع الناس لأن الصفوف تدخل وتخرج أثناء التمرير. إذا بدأ كل صف فك ترميز صور، تحليل JSON، أو قراءة قاعدة بيانات في onAppear, ستحصل على ذروات متكررة.
انقل العمل الثقيل خارج الصف. احسب ما تستطيع عند تحميل البيانات، خزّن النتائج، واجعل onAppear محدودًا إلى إجراءات رخيصة (مثل تشغيل الترقيم عند الاقتراب من النهاية).
3) ربط القائمة كلها بتحريرات الصف
عندما يحصل كل صف على @Binding إلى مصفوفة كبيرة، يمكن أن يبدو تحرير صغير كتغيير كبير. هذا قد يجعل العديد من الصفوف تعيد تقييم نفسها، وأحيانًا تتجدد القائمة بأكملها.
فضل تمرير قيم غير قابلة للتغيير إلى الصف وإرسال التغييرات عبر إجراء خفيف الوزن (مثلاً، "قلب علامة المفضلة للمعرف id"). احتفظ بحالة كل صف داخليًا فقط عندما تنتمي فعلاً إليه.
4) الكثير من الرسوم المتحركة أثناء التمرير
الرسوم المتحركة مكلفة في القائمة لأنها قد تطلق عمليات قياس إضافية. تطبيق animation(.default, value:) عاليًا (على القائمة كلها) أو تحريك كل تغيير حالة صغير قد يجعل التمرير لزجًا.
اجعل الأمر بسيطًا:
- قصر الرسوم المتحركة على الصف الذي يتغير فقط
- تجنب التحريك أثناء التمرير السريع (خاصة للاختيار/التسليط)
- كن حذرًا مع الرسوم الضمنية على القيم المتغيرة بكثرة
- فضّل الانتقالات البسيطة على التأثيرات المركبة
مثال حقيقي: قائمة دردشة يبدأ فيها كل صف جلب شبكة في onAppear, يستخدم UUID() كـ id, ويؤثر على تغيّرات حالة "تم مشاهدته" برسوم متحركة. هذا المزيج يخلق اضطراب صفوف مستمر. إصلاح الهوية، التخزين المؤقت، وتقييد الرسوم عادةً يجعل نفس الواجهة تبدو فورًا أكثر سلاسة.
قائمة تدقيق سريعة، مثال بسيط، وخطوات تالية
إذا نفذت بضعة أشياء فقط، ابدأ هنا:
- استخدم
idفريدًا وثابتًا لكل صف (ليس مؤشر المصفوفة، وليس UUID مولّد حديثًا) - اجعل عمل الصف صغيرًا: تجنّب التنسيق الثقيل، شجر عرض كبير، وخصائص محسوبة مكلفة في
body - سيطر على النشرات: لا تدع الحالة سريعة التغير (مؤقتات، كتابة، تقدم) تبطل القائمة كلها
- حمل بالصفحات وفّعل التحميل المسبق حتى تبقى الذاكرة ثابتة
- قم بالقياس قبل وبعد باستخدام Instruments حتى لا تخمن
تخيل صندوق دعم به 20000 محادثة. كل صف يظهر موضوعًا، معاينة رسالة أخيرة، طابع زمني، شارة غير مقروءة، وصورة رمزية. يمكن للمستخدم البحث، وتصل رسائل جديدة أثناء التمرير. النسخة البطيئة عادةً تفعل عدة أشياء معًا: تعيد بناء الصفوف في كل ضغطة مفتاح، تعيد قياس النص كثيرًا، وتبدأ جلب صور كثيرة مبكرًا.
خطة عملية لا تتطلب تفكيك قاعدة الشيفرة:
- خط أساس: سجّل تمريرًا قصيرًا وجلسة بحث في Instruments (Time Profiler + Core Animation).
- أصلح الهوية: تأكد أن نموذجك يملك
idحقيقيًا من الخادم/قاعدة البيانات، وForEachيستخدمه باستمرار. - أضف الترقيم: ابدأ بأحدث 50 إلى 100 عنصر، ثم حمّل المزيد عند اقتراب المستخدم من النهاية.
- حسّن الصور: استخدم مصغرات أصغر، خزّن النتائج، وتجنّب فك الترميز على الخيط الرئيسي.
- أعد القياس: تحقق من عدد أقل من عمليات القياس، تحديثات عرض أقل، وأوقات إطار أكثر ثباتًا على الأجهزة الأقدم.
إذا تبنيت منتجًا كاملاً (تطبيق iOS بالإضافة إلى باك أند ولوحة إدارة ويب)، قد يساعد أيضًا تصميم نموذج البيانات وعقد الترقيم مبكرًا. منصات مثل AppMaster (appmaster.io) مبنية لتلك التجربة الشاملة: تستطيع تعريف البيانات والمنطق التجاري بصريًا، ولا تزال تولّد شيفرة مصدر حقيقية يمكنك نشرها أو استضافتها ذاتيًا.
الأسئلة الشائعة
ابدأ بإصلاح هوية الصفوف. استخدم id ثابتًا من النموذج ولا تولّد معرفات داخل العرض، لأن تغيير المعرفات يجبر SwiftUI على اعتبار الصفوف جديدة وإعادة بنائها أكثر مما يلزم.
إعادة حساب body عادةً ليست باهظة التكلفة؛ لكن ما يؤديه هذا الحساب هو المكلف. القياسات الثقيلة للواجهة، قياس النص، فك ترميز الصور، وإعادة بناء الكثير من الصفوف نتيجة هوية غير مستقرة هي ما يسبب فقدان الإطارات عادةً.
لا تستخدم UUID() داخل الصف أو تعتمد على مؤشرات المصفوفة كهوية إذا كان يمكن إدراج أو حذف أو إعادة ترتيب البيانات. فضّل معرفًا من الخادم/قاعدة البيانات أو UUID مخزّنًا على النموذج عند إنشائه، حتى يبقى المعرف نفسه عبر التحديثات.
نعم يمكن أن يجعل ذلك الأداء أسوأ، خصوصًا إذا تغيّر هاش القيمة عندما تتغير حقول قابلة للتحرير، لأن SwiftUI قد يراها كصف مختلف. إذا احتجت Hashable فاجعلها مبنية على معرف ثابت واحد بدل خصائص مثل name أو isSelected أو نص مشتق.
أخرج الأعمال المكلفة من body. قم بتنسيق التواريخ والأرقام مسبقًا، تجنّب إنشاء محولات (formatters) جديدة لكل صف، ولا تبنِ مصفوفات مشتقة كبيرة باستخدام map/filter داخل العرض؛ احسبها مرة واحدة في النموذج أو الـ ViewModel ومرّر قيماً صغيرة جاهزة للعرض إلى الصف.
onAppear يحدث أكثر مما تتوقع لأن الصفوف تدخل وتخرج من الشاشة أثناء التمرير. إذا بدأ كل صف عملًا ثقيلًا هناك (فك ترميز صور، قراءات قاعدة بيانات، تحليل)، ستحصل على ذروات متكررة؛ اجعل onAppear محدودًا لمهام رخيصة مثل تشغيل الترقيم عند الاقتراب من النهاية.
أي قيمة منشورة سريعة التغير ومشتركة مع القائمة يمكن أن تبطلها مرارًا، حتى إن لم تتغير بيانات الصفوف. أبقِ المؤقتات، حالة الكتابة، وتحديثات التقدم بعيدًا عن الكائن الرئيسي الذي يقود القائمة، استخدم نزع التذبذب (debounce) للبحث، وقسّم ObservableObject الكبير إلى أصغر عند الحاجة.
استخدم List عندما يكون تصميمك أقرب إلى جدولة قياسية (صفوف نصية وصور، إجراءات سحب، اختيار، فواصل). استخدم ScrollView + LazyVStack عندما تحتاج تخطيطات مخصصة، لكن قس الأداء والذاكرة لأنّه أسهل أن تجرّ عمل حسابي إضافي بطريق الخطأ.
ابدأ بالتحميل قبل أن يظهر الصف الأخير تمامًا عبر الانتقال إلى شحنة عندما يقترب المستخدم من النهاية، واحمِ نفسك من المشغلات المكررة. اجعل أحجام الصفحات معقولة، تتبّع isLoading وreachedEnd، وادمج النتائج بناءً على معرفات ثابتة لتفادي الصفوف المكررة والفرق غير الضروري.
سجل خط أساس على جهاز حقيقي واستخدم Instruments لإيجاد ذروات الخيط الرئيسي وتدفق التخصيصات أثناء التمرير السريع. Time Profiler يوضح ما يعرقل التمرير، Allocations يكشف إنشاء كائنات مؤقتة لكل صف، وCore Animation يؤكد فقدان الإطارات لتحديد ما إذا كانت المشكلة في العرض أو في العمل على البيانات.


