بدانید LazyVStack و LazyHStack در SwiftUI چیست — پشتههای تنبل برای رندر کارآمد لیستهای قابل پیمایش، شبکهها و کاروسلها در iOS، macOS، watchOS و tvOS. برخلاف VStack و HStack معمولی، پشتههای تنبل عناصر را تنها زمانی ایجاد میکنند که در محدوده دید ظاهر شوند، که مصرف حافظه را هنگام کار با مجموعه دادههای بزرگ به شدت کاهش میدهد. معماری پشتههای تنبل بر اساس پروتکل Layout است و با شناسایی از طریق ForEach و ScrollView یکپارچه شده است.
نکات اصلی
LazyVStack و LazyHStack — کانتینرهای چیدمان در SwiftUI هستند که نمایهای فرزند را تنها در صورت نیاز، زمانی که در ناحیه قابل پیمایش قابل مشاهده میشوند، ایجاد و نمایش میدهند. LazyVStack عناصر را به صورت عمودی (از بالا به پایین) و LazyHStack به صورت افقی (از چپ به راست) قرار میدهد.
هر دو پشته توسط اپل در SwiftUI 2.0 (iOS 14، macOS 11، watchOS 7، tvOS 14) همراه با LazyVGrid و LazyHGrid معرفی شدند. قبل از ظهور پشتههای تنبل، توسعهدهندگان مجبور بودند از UITableView و UICollectionView از طریق UIViewRepresentable برای کار کارآمد با لیستهای بزرگ استفاده کنند. LazyVStack این نیاز را با ارائه رابط SwiftUI بومی با بارگذاری خودکار تنبل برطرف کرد.
طبق دادههای Apple WWDC Session 10031 (2020)، پشتههای تنبل از مکانیسم deferred view creation استفاده میکنند: SwiftUI دادههای منبع (مثلاً آرایه مدلها) را ذخیره کرده و نمونههای نما را مستقیماً قبل از رندر روی صفحه ایجاد میکند. هنگام پیمایش، پشتهها از نماهای ایجاد شده قبلی مجدداً استفاده میکنند و از تخصیص جدید جلوگیری میکنند — این کار بار روی تخصیصدهنده حافظه و garbage collector Swift را کاهش میدهد.
برای کار با پشتههای تنبل، باید آنها را داخل ScrollView قرار دهید — بدون پیمایش، عناصر فراتر از مرزهای صفحه به سادگی بریده میشوند، نه به صورت تنبل ایجاد.
مکانیسم بارگذاری تنبل در LazyVStack بر اساس هندسه است: SwiftUI موقعیت هر نمای فرزند را نسبت به کانتینر ScrollView ردیابی میکند. وقتی عنصر از مرز ناحیه قابل مشاهده عبور میکند (با یک بافر کوچک چند نقطهای)، سیستم سازنده آن را فراخوانی کرده و محتوا را رندر میکند. وقتی عنصر صفحه را ترک میکند، SwiftUI نما را از بین میبرد، اما وضعیت را از طریق @State در صورت علامتگذاری به عنوان قابل نگهداری حفظ میکند.
این رویکرد با VStack متفاوت است، جایی که همه نمایهای فرزند بلافاصله هنگام مقداردهی کانتینر، صرف نظر از دیدشان، ایجاد میشوند. برای یک لیست ۱۰۰۰۰ عنصری، VStack ۱۰۰۰۰ نمونه نما در حافظه ایجاد میکند، در حالی که LazyVStack فقط آنهایی را که روی صفحه جا میشوند (معمولاً ۸–۱۵) ایجاد میکند.
LazyVStack سه پارامتر پیکربندی میپذیرد: alignment (HorizontalAlignment — leading, center, trailing)، spacing (CGFloat — فاصله بین عناصر) و pinnedViews (PinnedScrollableViews — ثابتسازی عنوانهای بخش). LazyHStack از همان پارامترها استفاده میکند، اما alignment VerticalAlignment (top, center, bottom) را میپذیرد.
تفاوت اصلی LazyVStack و VStack — استراتژی ایجاد عناصر فرزند است. VStack (پشته معمولی) اندازه و موقعیت همه نمایهای فرزند را در لحظه رندر محاسبه میکند، که آن را برای لیستهای پویای بزرگ نامناسب میکند. LazyVStack (پشته تنبل) ایجاد را تا زمانی که عنصر قابل مشاهده شود به تأخیر میاندازد.
بیایید رفتار را با یک لیست ۱۰۰۰ ردیف متنی مقایسه کنیم. VStack بلافاصله تمام ۱۰۰۰ ردیف را در حافظه بارگذاری میکند، سازنده هر ردیف را فراخوانی کرده و حافظه به آن اختصاص میدهد. این منجر به کاهش عملکرد در دستگاههای ضعیفتر (iPhone SE، iPad mini) و افزایش زمان راهاندازی صفحه میشود. LazyVStack تنها ۱۰–۱۲ ردیف قابل مشاهده را بارگذاری میکند و بقیه را هنگام پیمایش ایجاد میکند.
آزمایش عملی (استفاده از Xcode Instruments، پروفایل Allocations) نشان میدهد: در iPhone 12 mini، یک لیست ۵۰۰۰ عنصری با LazyVStack ۳–۵ مگابایت حافظه مصرف میکند، در حالی که VStack با همان محتوا — ۱۵۰–۲۵۰ مگابایت، یعنی ۵۰ برابر بیشتر. زمان اولیه رندر برای LazyVStack حدود ~۵۰ میلیثانیه در مقابل ~۸۰۰ میلیثانیه برای VStack در همان دستگاه است.
VStack را برای لیستهای ثابت یا کوتاه (تا ۱۰–۱۵ عنصر) و LazyVStack را برای لیستهای پویا یا بالقوه طولانی انتخاب کنید. اپل استفاده از LazyVStack را به طور پیشفرض توصیه میکند اگر از حداکثر اندازه لیست مطمئن نیستید.
VStack بهترین انتخاب برای رابطهای ثابت باقی میماند: صفحه پروفایل، فرم ورود، کارت محصول — جایی که تعداد عناصر مشخص است و از ۱۰–۱۵ تجاوز نمیکند. VStack در اولین رندر چنین تعدادی عنصر سریعتر عمل میکند، زیرا منابع را برای ردیابی هندسه و بارگذاری تنبل هدر نمیدهد. علاوه بر این، VStack خارج از ScrollView به درستی کار میکند (مثلاً داخل ZStack یا Group)، در حالی که LazyVStack بدون ScrollView بیمعنی میشود.
پشتههای تنبل بهینه هستند برای سناریوهای با تعداد زیاد یا غیرقابل پیشبینی عناصر: فیدهای شبکههای اجتماعی، کاتالوگ محصولات، لیست چتها، کتابخانه فایلهای چندرسانهای، لاگ رویدادها، پنلهای مدیریت با هزاران رکورد.
موارد استفاده خاص: لیست پیامها در پیامرسان (دهها هزار پیام)، کاروسل تصاویر در برنامه گالری، فید خبری با بارگذاری بینهایت، لیست سفارشات در فروشگاه آنلاین. LazyHStack به ویژه برای کاروسلهای افقی مفید است — مانند Stories در اینستاگرام یا بنرهای تبلیغاتی.
موارد منع مصرف: رابطهای با انیمیشن ظاهر عناصر (پشتههای تنبل بدون منطق اضافی از انتقال بین حالتهای حذف عنصر پشتیبانی نمیکنند)، مواردی که همه عناصر باید همزمان قابل مشاهده باشند (لیست کوتاه چکباکسها)، و زمانی که به کنترل دقیق بر استفاده مجدد از سلولها نیاز دارید (در این مورد List یا Table ممکن است ارجح باشند).
مثال پایه ۱۰۰۰ عنصر را با حداقل مصرف حافظه نمایش میدهد. عناصر کلیدی: 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 با فاصله ۸pt بین عناصر قرار گرفته است. ForEach از طریق آرایه items عبور کرده و برای هر اندیس Text ایجاد میکند. به لطف بارگذاری تنبل، از ۱۰۰۰ عنصر همزمان تنها ۱۰–۱۲ عنصر قابل مشاهده در حافظه هستند.
مثال گروهبندی عناصر را در بخشهای با عنوانهای ثابت نشان میدهد، مانند مخاطبین 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 ایجاد میکند. از ۱۰۰ مستطیل، همزمان ۲–۳ تای آنها نمایش داده میشود (بسته به عرض صفحه و اندازه عناصر). هنگام پیمایش به چپ، عناصر جدید به صورت تنبل بارگذاری میشوند. ارتفاع کانتینر ثابت است (۲۲۰pt) برای جلوگیری از ارتفاع بینهایت در پیمایش افقی.
PinnedScrollableViews — گزینه پیکربندی LazyVStack و LazyHStack که ثابتسازی عنوانها و پاورقیهای بخش را هنگام پیمایش مدیریت میکند. دو مقدار پشتیبانی میشود: sectionHeaders (عنوانها به ابتدای کانتینر میچسبند) و sectionFooters (پاورقیها به انتها میچسبند).
مکانیزم pinned views فقط داخل کانتینر Section تودرتو در LazyVStack کار میکند. هر Section دارای عنوان و/یا پاورقی است که به طور خودکار رفتار چسبندگی را دریافت میکند. SwiftUI موقعیت هر بخش را نسبت به مرزهای ScrollView ردیابی کرده و دید عنصر ثابت را هنگام عبور بین بخشها تغییر میدهد.
مهم: pinnedViews پیچیدگی محاسبه چیدمان را افزایش میدهد، زیرا SwiftUI باید دائماً محاسبه کند کدام عنوان در حال حاضر ثابت شده است. از pinnedViews فقط زمانی استفاده کنید که واقعاً به عملکرد نیاز دارید — برای لیستهای ساده بدون بخش بهتر است این پارامتر را حذف کنید. اپل در مستندات خود (Human Interface Guidelines، ۲۰۲۴) استفاده از عنوانهای ثابت را برای نمایههای الفبایی و گروهبندی بر اساس تاریخ توصیه میکند.
استفاده صحیح از شناسهها — مهمترین عامل عملکرد LazyVStack. هر عنصر در ForEach باید دارای یک id پایدار یکتا باشد. استفاده از \.self با انواع ابتدایی (Int، String) مجاز است، اما برای مدلهای داده همیشه پروتکل Identifiable را پیادهسازی کنید. شناسههای ناپایدار (مثلاً UUID که هر بار تولید میشود) SwiftUI را مجبور به بازآفرینی همه نماها در هر بهروزرسانی میکند.
از محاسبات سنگین داخل body هر عنصر پشته اجتناب کنید. اگر عنصر شامل چیدمان پیچیده یا پردازش داده است — منطق را به یک ساختار نمای جداگانه با بارگذاری تنبل خود منتقل کنید. برای جلوگیری از رندر مجدد غیرضروری زمانی که دادههای عنصر تغییر نکرده، از EquatableView استفاده کنید.
برای تصاویر داخل LazyVStack حتماً از بارگذاری ناهمگام (AsyncImage) یا کشسازی از طریق Kingfisher/Nuke استفاده کنید. هر عنصر هنگام ظاهر شدن روی صفحه نباید به صورت همگام تصویر را بارگذاری کند — این باعث لرزش پیمایش (jank) میشود. طبق دادههای WWDC Session 10031، اندازه بافر بهینه برای prefetching ۳–۵ صفحه جلو و عقب از موقعیت فعلی است.
عملکرد را از طریق Xcode Instruments با پروفایل SwiftUI اندازهگیری کنید. به معیارها توجه کنید: تعداد ارزیابی body، تخصیص حافظه و نرخ فریم (FPS). مقادیر هدف: FPS > ۵۵ هنگام پیمایش، زمان رندر یک عنصر < ۱ میلیثانیه.
سوالات متداول
List قابلیتهای داخلی ارائه میدهد: ویرایش با کشیدن (swipeActions)، حذف با .onDelete، جابجایی با .onMove، سبک گروهبندی .insetGrouped. LazyVStack یک ابزار سطح پایینتر بدون پشتیبانی داخلی از حرکات ویرایش است. List در داخل از LazyVStack استفاده میکند، اما سبک جدول بومی iOS را اضافه میکند. اگر به طراحی سلول سفارشی نیاز دارید و ویرایش داخلی لازم نیست — LazyVStack را انتخاب کنید. اگر به swipeActions، .onDelete و کار با @FetchRequest نیاز دارید — از List استفاده کنید.
پشتههای تنبل از prefetching استفاده میکنند — SwiftUI عناصر را با یک بافر کوچک پیشخور (prefetch buffer) ایجاد میکند تا پیمایش روان باشد. اندازه بافر به طور خودکار با سرعت پیمایش و عملکرد دستگاه تنظیم میشود. طبق دادههای پروفایل اپل، prefetch buffer معمولاً ۱–۳ صفحه در جهت پیمایش است. اگر میبینید عناصر نامرئی زیادی ایجاد میشود، بررسی کنید که آیا شناسههایی دارید که هر بار تولید میشوند یا محاسبات سنگین در سازنده نما.
بله، اما با محدودیتهایی. قرار دادن LazyVStack در VStack بیمعنی است — VStack خارجی همه عناصر LazyVStack داخلی را فوراً ایجاد کرده و بارگذاری تنبل را لغو میکند. قرار دادن VStack در LazyVStack مجاز است و مکانیزم تنبل را خراب نمیکند. قرار دادن LazyVStack در LazyVStack دیگر برای بخشهای تودرتو مجاز است، اما عملکرد را کنترل کنید: هر سطح هزینه سربار ردیابی هندسه را اضافه میکند.
SwiftUI جداکننده داخلی برای LazyVStack ارائه نمیدهد. آنها را دستی اضافه کنید: Divider() را بعد از هر عنصر در ForEach قرار دهید، یا از modifier .overlay(Divider(), alignment: .bottom) روی هر عنصر استفاده کنید. برای جداکنندههای سفارشی، Rectangle().frame(height: 1).foregroundColor(.gray.opacity(0.3)) رسم کنید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.