Научете какво са 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), мързеливите стекове използват механизма deferred view creation: SwiftUI съхранява изходни данни (напр. масив от модели) и създава инстанции на изгледи непосредствено преди рендиране на екрана. При превъртане, стековете повторно използват вече създадени изгледи, избягвайки нови алокации — това намалява натоварването на алокатора на памет и garbage collector на 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 ms в сравнение с ~800 ms за 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("Element #\(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. Всяка секция има заглавна част и/или долна част, които автоматично получават поведение на залепване. SwiftUI проследява позицията на всяка секция спрямо границите на ScrollView и превключва видимостта на закрепения елемент при преход между секции.
Важно: pinnedViews увеличават сложността на изчисляване на оформлението, тъй като SwiftUI трябва постоянно да преизчислява коя заглавна част е в момента закрепена. Използвайте pinnedViews само когато функционалността наистина е необходима — за прости списъци без секции е по-добре да пропуснете този параметър. Apple в своята документация (Human Interface Guidelines, 2024) препоръчва използването на закрепени заглавни части за азбучни индекси и групиране по дати.
Правилно използване на идентификатори — най-важният фактор за производителност на LazyVStack. Всеки елемент в ForEach трябва да има стабилен уникален id. Използването на \.self с примитивни типове (Int, String) е допустимо, но за модели данни винаги имплементирайте Identifiable протокол. Нестабилни id-та (напр. UUID, генериран всеки път) принуждават SwiftUI да пресъздава всички изгледи при всяка актуализация.
Избягвайте тежки изчисления вътре в body на всеки елемент от стека. Ако елементът съдържа сложно оформление или обработка на данни — преместете логиката в отделна структура на изглед със собствено мързеливо зареждане. Използвайте EquatableView за предотвратяване на ненужни прерисувания, когато данните на елемента не са се променили.
За изображения вътре в LazyVStack задължително прилагайте асинхронно зареждане (AsyncImage) или кеширане чрез Kingfisher/Nuke. Всеки елемент, появяващ се на екрана, не трябва да зарежда синхронно изображение — това ще причини заекване при превъртане (jank). Според данни от WWDC Session 10031, оптималният размер на буфера за prefetching е 3–5 екрана напред и назад от текущата позиция.
Измервайте производителността чрез Xcode Instruments с профил SwiftUI. Обърнете внимание на метриките: брой оценявания на body, алокации и честота на кадрите (FPS). Целеви стойности: FPS > 55 при превъртане, време за рендиране на един елемент < 1 ms.
Често задавани въпроси
List предоставя вградени възможности: редактиране с плъзгане (swipeActions), изтриване чрез .onDelete, преместване чрез .onMove, стил на групиране .insetGrouped. LazyVStack е инструмент от по-ниско ниво без вградена поддръжка за жестове за редактиране. List използва LazyVStack вътрешно, но добавя роден iOS стил на таблица. Ако имате нужда от персонализиран дизайн на клетка и не се нуждаете от вградено редактиране — изберете LazyVStack. Ако имате нужда от swipeActions, .onDelete и работа с @FetchRequest — използвайте List.
Мързеливите стекове използват prefetching — SwiftUI създава елементи с малко предварително изпреварване (prefetch buffer), за да бъде превъртането плавно. Размерът на буфера се настройва автоматично спрямо скоростта на превъртане и производителността на устройството. Според данни от профилиране на Apple, prefetch buffer обикновено е 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също