تعرف على ما هما LazyVStack و LazyHStack في SwiftUI — الرصيفات البطيئة للعرض الفعال للقوائم القابلة للتمرير والشبكات والدوارات على iOS و macOS و watchOS و tvOS. على عكس VStack و HStack العاديين، تنشئ الرصيفات البطيئة العناصر فقط عند ظهورها في المنطقة المرئية، مما يقلل بشكل كبير من استهلاك الذاكرة عند العمل مع مجموعات البيانات الكبيرة. تعتمد بنية الرصيفات البطيئة على بروتوكول Layout وتتكامل مع التحديد عبر ForEach و ScrollView.
النقاط الرئيسية
LazyVStack و LazyHStack هما حاويات تخطيط في SwiftUI تنشئ وتعرض العروض الفرعية فقط عند الحاجة، عندما تصبح مرئية في المنطقة القابلة للتمرير. يرتب LazyVStack العناصر عمودياً (من أعلى إلى أسفل)، بينما يرتب LazyHStack العناصر أفقياً (من اليسار إلى اليمين).
تم تقديم كلتا الرصيفتين بواسطة Apple في SwiftUI 2.0 (iOS 14، macOS 11، watchOS 7، tvOS 14) إلى جانب LazyVGrid و LazyHGrid. قبل ظهور الرصيفات البطيئة، كان على المطورين استخدام UITableView و UICollectionView عبر UIViewRepresentable للعمل بكفاءة مع القوائم الكبيرة. أزال LazyVStack هذه الحاجة بتوفير واجهة SwiftUI أصلية مع تحميل بطيء تلقائي.
وفقاً لجلسة Apple WWDC Session 10031 (2020)، تستخدم الرصيفات البطيئة آلية إنشاء عرض مؤجل: يخزن SwiftUI بيانات المصدر (مثل مصفوفة من النماذج) وينشئ مثيلات العرض قبل العرض على الشاشة مباشرة. عند التمرير، تعيد الرصيفات استخدام العروض المنشأة بالفعل، متجنبة التخصيصات الجديدة — مما يقلل الحمل على مخصص الذاكرة ومجمّع القمامة في Swift.
للعمل مع الرصيفات البطيئة، ضعها دائماً داخل ScrollView — بدون تمرير، سيتم قص العناصر التي تمتد خارج حدود الشاشة، ولن يتم إنشاؤها بشكل بطيء.
آلية التحميل البطيء في LazyVStack تعتمد على الهندسة: يتتبع SwiftUI موضع كل عرض فرعي بالنسبة لحاوية ScrollView. عندما يعبر عنصر حدود المنطقة المرئية (مع مخزن مؤقت صغير من بضع نقاط)، يستدعي النظام المُهيئ الخاص به ويعرض المحتوى. عندما يغادر عنصر الشاشة، يدمر SwiftUI العرض لكنه يحتفظ بالحالة عبر @State إذا كانت مُعلّمة على أنها قابلة للحفظ.
يختلف هذا النهج عن VStack، حيث يتم إنشاء جميع العروض الفرعية فوراً عند تهيئة الحاوية، بغض النظر عن رؤيتها. لقائمة من 10,000 عنصر، سينشئ VStack 10,000 مثيل عرض في الذاكرة، بينما سينشئ LazyVStack فقط تلك التي تتسع على الشاشة (عادةً 8–15).
يقبل LazyVStack ثلاث معلمات تكوين: alignment (HorizontalAlignment — leading، center، trailing)، spacing (CGFloat — المسافة بين العناصر)، و pinnedViews (PinnedScrollableViews — تثبيت رؤوس الأقسام). يستخدم LazyHStack نفس المعلمات، لكن alignment يقبل VerticalAlignment (top، center، bottom).
الفرق الرئيسي بين LazyVStack و VStack هو استراتيجية إنشاء العناصر الفرعية. يحسب VStack (الرصيفة الحريصة) حجم وموضع جميع العروض الفرعية في وقت العرض، مما يجعله غير مناسب للقوائم الديناميكية الكبيرة. يؤجل LazyVStack (الرصيفة البطيئة) الإنشاء حتى يصبح العنصر مرئياً.
لنقارن السلوك باستخدام قائمة من 1000 سطر نص. سيحمّل VStack جميع الأسطر الـ 1000 في الذاكرة فوراً، مستدعياً مُهيئ كل سطر ومخصصاً ذاكرة له. يؤدي هذا إلى تدهور الأداء على الأجهزة الضعيفة (iPhone SE، iPad mini) وزيادة وقت بدء تشغيل الشاشة. سيحمّل LazyVStack فقط الأسطر المرئية البالغ عددها 10–12، منشئاً الباقي أثناء التمرير.
يظهر اختبار عملي (باستخدام Xcode Instruments، ملف Allocations): على iPhone 12 mini، قائمة من 5000 عنصر مع LazyVStack تستهلك 3–5 MB من الذاكرة، بينما VStack بنفس المحتوى تستهلك 150–250 MB — أي 50 ضعفاً. في الوقت نفسه، وقت العرض الأولي لـ LazyVStack هو ~50 مللي ثانية مقابل ~800 مللي ثانية لـ VStack على نفس الجهاز.
اختر VStack للقوائم الثابتة أو القصيرة (حتى 10–15 عنصراً)، و LazyVStack لأي قوائم ديناميكية أو طويلة محتملة. توصي Apple باستخدام LazyVStack افتراضياً إذا لم تكن متأكداً من الحد الأقصى لحجم القائمة.
يظل VStack الخيار الأفضل للواجهات الثابتة: شاشة الملف الشخصي، نموذج تسجيل الدخول، بطاقة المنتج — حيث يكون عدد العناصر معروفاً ولا يتجاوز 10–15. يعمل VStack بشكل أسرع في العرض الأولي لمثل هذه الكميات لأنه لا يهدر الموارد على تتبع الهندسة والتحميل البطيء. بالإضافة إلى ذلك، يعمل VStack بشكل صحيح خارج ScrollView (مثلاً داخل ZStack أو Group)، بينما LazyVStack بدون ScrollView يفقد غرضه.
الرصيفات البطيئة مثالية للسيناريوهات ذات العدد الكبير أو غير المتوقع من العناصر: خلاصات وسائل التواصل الاجتماعي، كتالوجات المنتجات، قوائم الدردشة، مكتبات ملفات الوسائط، سجلات الأحداث، لوحات الإدارة بآلاف السجلات.
حالات استخدام محددة: قائمة الرسائل في تطبيق مراسلة (عشرات الآلاف من الرسائل)، دوارة الصور في تطبيق معرض، خلاصة الأخبار مع التحميل اللانهائي، قائمة الطلبات في متجر عبر الإنترنت. LazyHStack مفيد بشكل خاص للدوارات الأفقية — مثل Stories في Instagram أو اللافتات الترويجية.
موانع الاستخدام: واجهات ذات رسوم متحركة لظهور العناصر (الرصيفات البطيئة لا تدعم التحولات بين حالات حذف العناصر بدون منطق إضافي)، الحالات التي يجب أن تكون فيها جميع العناصر مرئية في وقت واحد (قائمة قصيرة من مربعات الاختيار)، وعندما تحتاج إلى تحكم دقيق في إعادة استخدام الخلايا (في هذه الحالة قد يكون List أو Table أفضل).
مثال أساسي يعرض 1000 عنصر باستهلاك أدنى للذاكرة. العناصر الرئيسية: ScrollView كحاوية تمرير، LazyVStack للتحميل البطيء، ForEach مع معرّف لتكرار البيانات.
import SwiftUI
struct LazyListExample: View {
let items = Array(0..<1000)
var body: some View {
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items, id: \.self) { index in
Text("العنصر #\(index)")
.font(.body)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
}
}
}
ينشئ الكود ScrollView يحتوي على LazyVStack بتباعد 8pt بين العناصر. يتكرر ForEach عبر مصفوفة items وينشئ Text لكل فهرس. بفضل التحميل البطيء، من أصل 1000 عنصر، فقط 10–12 عنصراً مرئياً موجودة في الذاكرة في وقت واحد.
يوضح هذا المثال تجميع العناصر حسب الأقسام مع رؤوس مثبتة، مشابهاً لجهات اتصال iOS. Section يحدد الرأس والمحتوى، pinnedViews: .sectionHeaders يثبت الرأس في أعلى الشاشة عند التمرير.
import SwiftUI
struct SectionedList: View {
let cities = ["موسكو", "لندن", "طوكيو", "نيويورك", "باريس"]
let countries = ["روسيا", "المملكة المتحدة", "اليابان", "الولايات المتحدة", "فرنسا"]
var body: some View {
ScrollView {
LazyVStack(pinnedViews: .sectionHeaders) {
Section(header: Text("مدن").font(.title).bold()) {
ForEach(cities, id: \.self) { city in
Text(city).padding(8)
}
}
Section(header: Text("بلدان").font(.title).bold()) {
ForEach(countries, id: \.self) { country in
Text(country).padding(8)
}
}
}
}
}
}
تتصرف الرؤوس المثبتة (.sectionHeaders) مثل section headers في UITableView: عند تمرير قسم، "يلتصق" الرأس بالحافة العلوية للشاشة حتى يختفي القسم بأكمله، ثم يستبدله رأس القسم التالي. يمكن دمج pinnedViews: .sectionHeaders و .sectionFooters في وقت واحد.
يستخدم LazyHStack للتمرير الأفقي — دوارات الصور، قوائم الفئات الأفقية. معامل alignment: .top يحاذي العناصر إلى الحافة العلوية.
import SwiftUI
struct HorizontalCarousel: View {
let colors: [Color] = [.red, .blue, .green, .orange, .purple, .pink]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16, alignment: .top) {
ForEach(0..<100, id: \.self) { index in
RoundedRectangle(cornerRadius: 12)
.fill(colors[index % colors.count])
.frame(width: 150, height: 200)
.overlay(Text("\(index + 1)").foregroundColor(.white).bold())
}
}
.padding(.horizontal)
}
.frame(height: 220)
}
}
ينشئ الكود ScrollView أفقياً مع LazyHStack. من أصل 100 مستطيل، يتم عرض 2–3 فقط في وقت واحد (حسب عرض الشاشة وحجم العناصر). عند التمرير لليسار، يتم تحميل العناصر الجديدة بشكل بطيء. ارتفاع الحاوية ثابت (220pt) لتجنب الارتفاع اللانهائي في التمرير الأفقي.
PinnedScrollableViews هو خيار تكوين لـ LazyVStack و LazyHStack يتحكم في تثبيت رؤوس وأقدام الأقسام عند التمرير. يتم دعم قيمتين: sectionHeaders (تلتصق الرؤوس ببداية الحاوية) و sectionFooters (تلتصق الأقدام بالنهاية).
تعمل آلية pinned views فقط داخل حاوية Section متداخلة في LazyVStack. كل Section لها رأس و/أو قدم تحصل تلقائياً على سلوك الالتصاق. يتتبع SwiftUI موضع كل قسم بالنسبة لحدود ScrollView ويبدل رؤية العنصر المثبت عند الانتقال بين الأقسام.
هام: تزيد pinnedViews من تعقيد حساب التخطيط، حيث يجب على SwiftUI إعادة حساب الرأس المثبت حالياً باستمرار. استخدم pinnedViews فقط عندما تكون الوظيفة مطلوبة فعلياً — للقوائم البسيطة بدون أقسام، من الأفضل حذف هذه المعلمة. توصي Apple في توثيقها (Human Interface Guidelines، 2024) باستخدام الرؤوس المثبتة للفهارس الأبجدية والتجميع حسب التاريخ.
الاستخدام الصحيح للمعرفات هو أهم عامل أداء لـ LazyVStack. يجب أن يكون لكل عنصر في ForEach معرف فريد ومستقر. استخدام \.self مع الأنواع البدائية (Int، String) مقبول، لكن لنماذج البيانات قم دائماً بتنفيذ بروتوكول Identifiable. المعرفات غير المستقرة (مثل UUID المُنشأ في كل مرة) تجعل SwiftUI يعيد إنشاء جميع العروض في كل تحديث.
تجنب الحسابات الثقيلة داخل body لكل عنصر من الرصيفة. إذا كان العنصر يحتوي على تخطيط معقد أو معالجة بيانات — استخرج المنطق في هيكل عرض منفصل مع تحميل بطيء خاص به. استخدم EquatableView لمنع إعادة الرسم غير الضرورية عندما لا تتغير بيانات العنصر.
للصور داخل LazyVStack، استخدم دائماً التحميل غير المتزامن (AsyncImage) أو التخزين المؤقت عبر Kingfisher/Nuke. يجب ألا يقوم كل عنصر بتحميل صورة بشكل متزامن عند الظهور على الشاشة — سيؤدي ذلك إلى توقف التمرير. وفقاً لـ WWDC Session 10031، الحجم الأمثل لمخزن التحميل المسبق هو 3–5 شاشات للأمام والخلف من الموضع الحالي.
قس الأداء باستخدام Xcode Instruments مع ملف SwiftUI. انتبه للمقاييس: تقييمات body، التخصيصات، ومعدل الإطارات (FPS). القيم المستهدفة: FPS > 55 عند التمرير، وقت عرض عنصر واحد < 1 مللي ثانية.
الأسئلة الشائعة
List يوفر إمكانيات مدمجة: التحرير بالسحب (swipeActions)، الحذف عبر .onDelete، إعادة الترتيب عبر .onMove، النمط المجمع .insetGrouped. LazyVStack هو أداة منخفضة المستوى بدون دعم مدمج لإيماءات التحرير. يستخدم List LazyVStack داخلياً لكنه يضيف نمط الجدول الأصلي لنظام iOS. إذا كنت بحاجة لتصميم خلية مخصص ولا تحتاج لتحرير مدمج — اختر LazyVStack. إذا كنت بحاجة لـ swipeActions و .onDelete والعمل مع @FetchRequest — استخدم List.
تستخدم الرصيفات البطيئة التحميل المسبق — ينشئ SwiftUI عناصر بمخزن مؤقت صغير (prefetch buffer) لضمان تمرير سلس. يضبط حجم المخزن تلقائياً حسب سرعة التمرير وأداء الجهاز. وفقاً لبيانات Apple، مخزن التحميل المسبق عادة ما يكون 1–3 شاشات في اتجاه التمرير. إذا رأيت أنه يتم إنشاء عدد كبير جداً من العناصر غير المرئية، تحقق مما إذا كان لديك معرفات تُنشأ في كل مرة أو حسابات ثقيلة في مُهيئ العرض.
نعم، لكن مع قيود. تداخل LazyVStack داخل VStack لا معنى له — سينشئ VStack الخارجي جميع عناصر LazyVStack الداخلي فوراً، ملغياً التحميل البطيء. تداخل VStack داخل LazyVStack مقبول ولا يكسر الآلية البطيئة. تداخل LazyVStack داخل LazyVStack آخر مقبول للأقسام المتداخلة، لكن راقب الأداء: كل مستوى يضيف عبئاً إضافياً على تتبع الهندسة.
لا يوفر SwiftUI فواصل مدمجة لـ LazyVStack. أضفها يدوياً: ضع Divider() بعد كل عنصر في ForEach، أو استخدم المُعدِّل .overlay(Divider(), alignment: .bottom) على كل عنصر. للفواصل المخصصة، ارسم Rectangle().frame(height: 1).foregroundColor(.gray.opacity(0.3)).
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.