Aflați ce sunt LazyVStack și LazyHStack în SwiftUI — stive leneșe pentru randarea eficientă a listelor derulabile, grilelor și caruselurilor pe iOS, macOS, watchOS și tvOS. Spre deosebire de VStack și HStack obișnuite, stivele leneșe creează elemente doar când apar în zona de vizibilitate, ceea ce reduce critic consumul de memorie la lucrul cu seturi mari de date. Arhitectura stivelor leneșe se bazează pe protocolul Layout și este integrată cu identificarea prin ForEach și ScrollView.
Principalul
LazyVStack și LazyHStack — containere de layout în SwiftUI care creează și afișează vizualizări copil doar la nevoie, când devin vizibile în zona derulabilă. LazyVStack aranjează elementele vertical (de sus în jos), iar LazyHStack — orizontal (de la stânga la dreapta).
Ambele stive au fost introduse de Apple în SwiftUI 2.0 (iOS 14, macOS 11, watchOS 7, tvOS 14) împreună cu LazyVGrid și LazyHGrid. Înainte de apariția stivelor leneșe, dezvoltatorii erau nevoiți să folosească UITableView și UICollectionView prin UIViewRepresentable pentru a lucra eficient cu liste mari. LazyVStack a eliminat această necesitate, oferind o interfață nativă SwiftUI cu încărcare leneșă automată.
Conform datelor Apple WWDC Session 10031 (2020), stivele leneșe folosesc mecanismul deferred view creation: SwiftUI stochează datele sursă (de exemplu, un array de modele) și creează instanțe de vizualizări chiar înainte de randarea pe ecran. La derulare, stivele reutilizează vizualizările deja create, evitând alocarea de noi — aceasta reduce încărcarea pe alocatorul de memorie și garbage collector-ul Swift.
Pentru a lucra cu stivele leneșe, trebuie să le plasați în interiorul ScrollView — fără derulare, elementele care depășesc marginile ecranului vor fi pur și simplu tăiate, nu create leneș.
Mecanismul de încărcare leneșă în LazyVStack se bazează pe geometrie: SwiftUI urmărește poziția fiecărei vizualizări copil față de containerul ScrollView. Când un element traversează granița zonei vizibile (cu un mic buffer de câteva puncte), sistemul îi apelează inițializatorul și îi randă conținutul. Când elementul părăsește ecranul, SwiftUI distruge vizualizarea, dar păstrează starea prin @State, dacă este marcată ca păstrată.
Această abordare diferă de VStack, unde toate vizualizările copil sunt create imediat la inițializarea containerului, indiferent de vizibilitatea lor. Pentru o listă de 10 000 de elemente, VStack va crea 10 000 de instanțe de vizualizări în memorie, iar LazyVStack — doar pe cele care încap pe ecran (de obicei 8–15).
LazyVStack acceptă trei parametri de configurare: alignment (HorizontalAlignment — leading, center, trailing), spacing (CGFloat — spațiu între elemente) și pinnedViews (PinnedScrollableViews — fixarea antetelor secțiunilor). LazyHStack folosește aceiași parametri, dar alignment acceptă VerticalAlignment (top, center, bottom).
Diferența principală dintre LazyVStack și VStack — strategia de creare a elementelor copil. VStack (stivă obișnuită) calculează dimensiunea și poziția tuturor vizualizărilor copil în momentul randării, ceea ce o face nepotrivită pentru liste dinamice mari. LazyVStack (stivă leneșă) amână crearea până când elementul devine vizibil.
Să comparăm comportamentul pe exemplul unei liste de 1000 de rânduri de text. VStack va încărca toate cele 1000 de rânduri în memorie imediat, apelând inițializatorul fiecărui rând și alocându-i memorie. Aceasta duce la scăderea performanței pe dispozitivele slabe (iPhone SE, iPad mini) și la creșterea timpului de pornire a ecranului. LazyVStack va încărca doar rândurile vizibile 10–12, creând restul pe măsură ce derulați.
Testul practic (folosind Xcode Instruments, profilul Allocations) arată: pe iPhone 12 mini, o listă de 5000 de elemente cu LazyVStack consumă 3–5 MB de memorie, în timp ce VStack cu același conținut — 150–250 MB, de 50 de ori mai mult. Timpul inițial de randare pentru LazyVStack este de ~50 ms față de ~800 ms pentru VStack pe același dispozitiv.
Alegeți VStack pentru liste statice sau scurte (până la 10–15 elemente), iar LazyVStack — pentru orice liste dinamice sau potențial lungi. Apple recomandă utilizarea LazyVStack în mod implicit, dacă nu sunteți sigur de dimensiunea maximă a listei.
VStack rămâne cea mai bună alegere pentru interfețe statice: ecran de profil, formular de autentificare, card de produs — unde numărul de elemente este cunoscut și nu depășește 10–15. VStack funcționează mai rapid la prima randare a unui astfel de număr de elemente, deoarece nu consumă resurse pentru urmărirea geometriei și încărcare leneșă. În plus, VStack funcționează corect în afara ScrollView (de exemplu, în interiorul ZStack sau Group), în timp ce LazyVStack fără ScrollView își pierde sensul.
Stivele leneșe sunt optime pentru scenarii cu un număr mare sau imprevizibil de elemente: fluxuri de rețele sociale, cataloage de produse, liste de chat, biblioteci de fișiere media, jurnale de evenimente, panouri de administrare cu mii de înregistrări.
Cazuri concrete de utilizare: lista de mesaje într-un messenger (zeci de mii de mesaje), carusel de imagini într-o aplicație de galerie, flux de știri cu încărcare infinită, lista de comenzi într-un magazin online. LazyHStack este deosebit de util pentru caruseluri orizontale — cum ar fi Stories în Instagram sau bannere promoționale.
Contraindicații: interfețe cu animație de apariție a elementelor (stivele leneșe nu acceptă tranziții între stări de ștergere a elementelor fără logică suplimentară), cazuri în care toate elementele trebuie să fie vizibile simultan (listă scurtă de checkbox-uri) și când aveți nevoie de control precis asupra reutilizării celulelor (în acest caz, List sau Table pot fi preferabile).
Exemplul de bază afișează 1000 de elemente cu consum minim de memorie. Elemente cheie: ScrollView ca container de derulare, LazyVStack pentru încărcare leneșă, ForEach cu identificator pentru iterarea datelor.
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()
}
}
}
Codul creează un ScrollView, în interiorul căruia este plasat LazyVStack cu spațiere de 8pt între elemente. ForEach iterează prin array-ul items și creează Text pentru fiecare index. Datorită încărcării leneșe, din 1000 de elemente, simultan în memorie se află doar cele vizibile 10–12.
Exemplul demonstrează gruparea elementelor în secțiuni cu antete fixate, ca în contactele iOS. Section definește antetul și conținutul, pinnedViews: .sectionHeaders fixează antetul în partea de sus a ecranului la derulare.
import SwiftUI
struct SectionedList: View {
let cities = ["Moscova", "Londra", "Tokyo", "New York", "Paris"]
let countries = ["Rusia", "Marea Britanie", "Japonia", "SUA", "Franța"]
var body: some View {
ScrollView {
LazyVStack(pinnedViews: .sectionHeaders) {
Section(header: Text("Orașe").font(.title).bold()) {
ForEach(cities, id: \.self) { city in
Text(city).padding(8)
}
}
Section(header: Text("Țări").font(.title).bold()) {
ForEach(countries, id: \.self) { country in
Text(country).padding(8)
}
}
}
}
}
}
Antetele fixate (.sectionHeaders) se comportă ca section headers în UITableView: la derularea secțiunii, antetul se „lipește" de marginea superioară a ecranului până când întreaga secțiune dispare, după care este înlocuit de antetul secțiunii următoare. pinnedViews pot fi combinate: .sectionHeaders și .sectionFooters simultan.
LazyHStack este utilizat pentru derularea orizontală — caruseluri de imagini, liste orizontale de categorii. Parametrul alignment: .top aliniază elementele la marginea de sus.
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)
}
}
Codul creează un ScrollView orizontal cu LazyHStack. Din 100 de dreptunghiuri, simultan sunt afișate 2–3 (în funcție de lățimea ecranului și dimensiunea elementelor). La derularea spre stânga, elementele noi sunt încărcate leneș. Înălțimea containerului este fixă (220pt) pentru a evita înălțimea infinită în derularea orizontală.
PinnedScrollableViews — opțiunea de configurare a LazyVStack și LazyHStack care gestionează fixarea antetelor și subsolurilor secțiunilor la derulare. Sunt acceptate două valori: sectionHeaders (antetele se lipesc de începutul containerului) și sectionFooters (subsolurile se lipesc de sfârșit).
Mecanismul pinned views funcționează doar în interiorul containerului Section încastrat în LazyVStack. Fiecare secțiune are antet și/sau subsol care obțin automat comportamentul de lipire. SwiftUI urmărește poziția fiecărei secțiuni față de marginile ScrollView și comută vizibilitatea elementului fixat la trecerea între secțiuni.
Important: pinnedViews măresc complexitatea calculării layout-ului, deoarece SwiftUI trebuie să recalculeze constant care antet este fixat. Folosiți pinnedViews doar când funcționalitatea este cu adevărat necesară — pentru liste simple fără secțiuni, este mai bine să omiteți acest parametru. Apple în documentația sa (Human Interface Guidelines, 2024) recomandă utilizarea antetelor fixate pentru indici alfabetici și gruparea după date.
Utilizarea corectă a identificatorilor — cel mai important factor de performanță al LazyVStack. Fiecare element din ForEach trebuie să aibă un id unic stabil. Utilizarea \.self cu tipuri primitive (Int, String) este acceptabilă, dar pentru modelele de date implementați întotdeauna protocolul Identifiable. Id-urile instabile (de exemplu, UUID generat de fiecare dată) forțează SwiftUI să recrească toate vizualizările la fiecare actualizare.
Evitați calculele grele în interiorul body-ului fiecărui element al stivei. Dacă elementul conține un layout complex sau procesare de date — mutați logica într-o structură de vizualizare separată cu propria încărcare leneșă. Folosiți EquatableView pentru a preveni re-randările inutile atunci când datele elementului nu s-au schimbat.
Pentru imagini în interiorul LazyVStack, aplicați obligatoriu încărcare asincronă (AsyncImage) sau cache prin Kingfisher/Nuke. Fiecare element care apare pe ecran nu ar trebui să încarce sincron o imagine — aceasta va cauza întârzieri la derulare (jank). Conform datelor WWDC Session 10031, dimensiunea optimă a buffer-ului de prefetching este de 3–5 ecrane înainte și înapoi de la poziția curentă.
Măsurați performanța prin Xcode Instruments cu profilul SwiftUI. Acordați atenție metricilor: numărul de evaluări body, alocări și rata de cadre (FPS). Valorile țintă: FPS > 55 la derulare, timpul de randare a unui element < 1 ms.
Întrebări frecvente
List oferă funcționalități încorporate: editare prin glisare (swipeActions), ștergere prin .onDelete, reordonare prin .onMove, stil de grupare .insetGrouped. LazyVStack este un instrument de nivel inferior fără suport încorporat pentru gesturi de editare. List folosește LazyVStack intern, dar adaugă stilul nativ de tabel iOS. Dacă aveți nevoie de un design personalizat al celulei și nu aveți nevoie de editare încorporată — alegeți LazyVStack. Dacă aveți nevoie de swipeActions, .onDelete și lucru cu @FetchRequest — folosiți List.
Stivele leneșe folosesc prefetching — SwiftUI creează elemente cu un mic avans (prefetch buffer) pentru ca derularea să fie lină. Dimensiunea buffer-ului se ajustează automat în funcție de viteza de derulare și performanța dispozitivului. Conform datelor de profilare Apple, prefetch buffer este de obicei de 1–3 ecrane în direcția de derulare. Dacă vedeți că se creează prea multe elemente invizibile, verificați dacă aveți identificatori generați de fiecare dată sau calcule grele în inițializatorul vizualizării.
Da, dar cu limitări. Încastrarea LazyVStack în VStack nu are sens — VStack-ul exterior va crea toate elementele LazyVStack-ului interior imediat, anulând încărcarea leneșă. Încastrarea VStack în LazyVStack este permisă și nu strică mecanismul leneș. Încastrarea LazyVStack în alt LazyVStack este acceptabilă pentru secțiuni încastrate, dar urmăriți performanța: fiecare nivel adaugă costuri suplimentare pentru urmărirea geometriei.
SwiftUI nu oferă separatoare încorporate pentru LazyVStack. Adăugați-le manual: plasați Divider() după fiecare element în ForEach sau folosiți modifikatorul .overlay(Divider(), alignment: .bottom) pe fiecare element. Pentru separatoare personalizate, desenați Rectangle().frame(height: 1).foregroundColor(.gray.opacity(0.3)).
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și