LazyVStack: czym są pionowe i poziome leniwe stosy w SwiftUI

Autor: IT Sectr Opublikowano: 2026-02-24 Czas czytania: 10 min

Dowiedz się, czym są LazyVStack i LazyHStack w SwiftUI — leniwe stosy do efektywnego renderowania przewijanych list, siatek i karuzel na iOS, macOS, watchOS i tvOS. W przeciwieństwie do zwykłych VStack i HStack, leniwe stosy tworzą elementy dopiero gdy pojawiają się w obszarze widoczności, co krytycznie zmniejsza zużycie pamięci przy pracy z dużymi zbiorami danych. Architektura leniwych stosów opiera się na protokole Layout i jest zintegrowana z identyfikacją przez ForEach i ScrollView.

Najważniejsze

  • Leniwe ładowanie — LazyVStack i LazyHStack tworzą widoki potomne tylko wtedy, gdy wchodzą w obszar widoczności ScrollView, oszczędzając pamięć i poprawiając FPS podczas przewijania.
  • Identyfikacja przez id — dla poprawnej pracy z leniwymi kontenerami każdy element w ForEach musi mieć unikalny identyfikator (id: \.self lub protokół Identifiable).
  • pinnedViews — parametr umożliwia przypinanie nagłówków sekcji (sectionHeaders, sectionFooters) do górnej lub dolnej części ekranu podczas przewijania, analogicznie do UITableView section headers.
  • spacing i alignment — oba stosy przyjmują niestandardowy odstęp między elementami i wyrównanie (leading, center, trailing dla VStack; top, center, bottom dla HStack).
  • Zagnieżdżanie z GridItem — LazyVGrid i LazyHGrid są zbudowane na tej samej zasadzie leniwego ładowania, ale z obsługą kolumn i wierszy przez GridItem.

Czym są LazyVStack i LazyHStack?

LazyVStack i LazyHStack to kontenery układu w SwiftUI, które tworzą i wyświetlają widoki potomne tylko w razie potrzeby, gdy stają się widoczne w obszarze przewijanym. LazyVStack umieszcza elementy pionowo (od góry do dołu), a LazyHStack — poziomo (od lewej do prawej).

Oba stosy zostały wprowadzone przez Apple w SwiftUI 2.0 (iOS 14, macOS 11, watchOS 7, tvOS 14) razem z LazyVGrid i LazyHGrid. Przed pojawieniem się leniwych stosów programiści musieli używać UITableView i UICollectionView przez UIViewRepresentable do efektywnej pracy z dużymi listami. LazyVStack wyeliminował tę potrzebę, zapewniając natywny interfejs SwiftUI z automatycznym leniwym ładowaniem.

Według danych z Apple WWDC Session 10031 (2020), leniwe stosy wykorzystują mechanizm deferred view creation: SwiftUI przechowuje dane źródłowe (np. tablicę modeli) i tworzy instancje widoków bezpośrednio przed renderowaniem na ekranie. Podczas przewijania stosy ponownie wykorzystują już utworzone widoki, unikając alokacji nowych — zmniejsza to obciążenie alokatora pamięci i garbage collectora Swift.

Do pracy z leniwymi stosami należy umieszczać je wewnątrz ScrollView — bez przewijania elementy wykraczające poza granice ekranu zostaną po prostu przycięte, a nie utworzone leniwie.

Jak działa leniwe ładowanie

Mechanizm leniwego ładowania w LazyVStack opiera się na geometrii: SwiftUI śledzi pozycję każdego widoku potomnego względem kontenera ScrollView. Gdy element przekracza granicę widocznego obszaru (z niewielkim buforem kilku punktów), system wywołuje jego inicjalizator i renderuje zawartość. Gdy element opuszcza ekran, SwiftUI niszczy widok, ale zachowuje stan przez @State, jeśli jest oznaczony jako zachowywalny.

To podejście różni się od VStack, gdzie wszystkie widoki potomne są tworzone natychmiast podczas inicjalizacji kontenera, niezależnie od ich widoczności. Dla listy 10 000 elementów VStack utworzy 10 000 instancji widoków w pamięci, a LazyVStack — tylko te mieszczące się na ekranie (zwykle 8–15).

Rozmiary i wyrównanie

LazyVStack przyjmuje trzy parametry konfiguracyjne: alignment (HorizontalAlignment — leading, center, trailing), spacing (CGFloat — odstęp między elementami) i pinnedViews (PinnedScrollableViews — przypinanie nagłówków sekcji). LazyHStack używa tych samych parametrów, ale alignment przyjmuje VerticalAlignment (top, center, bottom).

Różnice między LazyVStack a VStack: wydajność i pamięć

Główna różnica między LazyVStack a VStack to strategia tworzenia elementów potomnych. VStack (eager stack) oblicza rozmiar i położenie wszystkich widoków potomnych w momencie renderowania, co czyni go nieodpowiednim dla dużych dynamicznych list. LazyVStack (lazy stack) odkłada tworzenie do momentu, gdy element staje się widoczny.

Porównajmy zachowanie na przykładzie listy 1000 wierszy tekstu. VStack załaduje wszystkie 1000 wierszy do pamięci natychmiast, wywołując inicjalizator każdego wiersza i alokując dla niego pamięć. Prowadzi to do spadku wydajności na słabszych urządzeniach (iPhone SE, iPad mini) i wydłużenia czasu uruchamiania ekranu. LazyVStack załaduje tylko widoczne 10–12 wierszy, tworząc pozostałe podczas przewijania.

Praktyczny test (użycie Xcode Instruments, profil Allocations) pokazuje: na iPhone 12 mini lista 5000 elementów z LazyVStack zużywa 3–5 MB pamięci, podczas gdy VStack z tą samą zawartością — 150–250 MB, co jest 50 razy więcej. Początkowy czas renderowania dla LazyVStack wynosi ~50 ms w porównaniu do ~800 ms dla VStack na tym samym urządzeniu.

Wybieraj VStack dla statycznych lub krótkich list (do 10–15 elementów), a LazyVStack — dla wszelkich dynamicznych lub potencjalnie długich list. Apple zaleca używanie LazyVStack domyślnie, jeśli nie jesteś pewien maksymalnego rozmiaru listy.

Kiedy VStack jest nadal potrzebny

VStack pozostaje najlepszym wyborem dla statycznych interfejsów: ekran profilu, formularz logowania, karta produktu — gdzie liczba elementów jest znana i nie przekracza 10–15. VStack działa szybciej przy pierwszym renderowaniu takiej liczby elementów, ponieważ nie zużywa zasobów na śledzenie geometrii i leniwe ładowanie. Ponadto VStack działa poprawnie poza ScrollView (np. wewnątrz ZStack lub Group), podczas gdy LazyVStack bez ScrollView traci sens.

Kiedy używać LazyVStack i LazyHStack

Leniwe stosy są optymalne dla scenariuszy z dużą lub nieprzewidywalną liczbą elementów: kanały mediów społecznościowych, katalogi produktów, listy czatów, biblioteki plików multimedialnych, dzienniki zdarzeń, panele administracyjne z tysiącami rekordów.

Konkretne przypadki użycia: lista wiadomości w komunikatorze (dziesiątki tysięcy wiadomości), karuzela obrazów w aplikacji galerii, kanał newsów z nieskończonym przewijaniem, lista zamówień w sklepie internetowym. LazyHStack jest szczególnie przydatny do poziomych karuzel — jak Stories w Instagramie lub banery promocyjne.

Przeciwwskazania: interfejsy z animacją pojawiania się elementów (leniwe stosy nie obsługują przejść między stanami usuwania elementów bez dodatkowej logiki), przypadki gdy wszystkie elementy powinny być widoczne jednocześnie (krótka lista pól wyboru) oraz gdy potrzebujesz precyzyjnej kontroli nad ponownym użyciem komórek (w tym przypadku List lub Table mogą być preferowane).

Przykłady kodu: lista, siatka i sekcje

Przykład 1: Prosta lista pionowa z LazyVStack

Podstawowy przykład wyświetla 1000 elementów przy minimalnym zużyciu pamięci. Kluczowe elementy: ScrollView jako kontener przewijania, LazyVStack do leniwego ładowania, ForEach z identyfikatorem do iteracji danych.

swift
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()
        }
    }
}

Kod tworzy ScrollView, wewnątrz którego umieszczono LazyVStack z odstępem 8pt między elementami. ForEach przechodzi przez tablicę items i tworzy Text dla każdego indeksu. Dzięki leniwemu ładowaniu z 1000 elementów jednocześnie w pamięci znajdują się tylko widoczne 10–12.

Przykład 2: Sekcje z pinnedViews

Przykład demonstruje grupowanie elementów w sekcje z przypiętymi nagłówkami, jak w kontaktach iOS. Section definiuje nagłówek i zawartość, pinnedViews: .sectionHeaders blokuje nagłówek u góry ekranu podczas przewijania.

swift
import SwiftUI

struct SectionedList: View {
    let cities = ["Moskwa", "Londyn", "Tokio", "Nowy Jork", "Paryż"]
    let countries = ["Rosja", "Wielka Brytania", "Japonia", "Stany Zjednoczone", "Francja"]

    var body: some View {
        ScrollView {
            LazyVStack(pinnedViews: .sectionHeaders) {
                Section(header: Text("Miasta").font(.title).bold()) {
                    ForEach(cities, id: \.self) { city in
                        Text(city).padding(8)
                    }
                }
                Section(header: Text("Kraje").font(.title).bold()) {
                    ForEach(countries, id: \.self) { country in
                        Text(country).padding(8)
                    }
                }
            }
        }
    }
}

Przypięte nagłówki (.sectionHeaders) zachowują się jak section headers w UITableView: podczas przewijania sekcji nagłówek „przykleja się" do górnej krawędzi ekranu, aż cała sekcja zniknie, po czym zastępuje go nagłówek następnej sekcji. pinnedViews można łączyć: .sectionHeaders i .sectionFooters jednocześnie.

Przykład 3: Karuzela pozioma z LazyHStack

LazyHStack jest używany do przewijania poziomego — karuzeli obrazów, poziomych list kategorii. Parametr alignment: .top wyrównuje elementy do górnej krawędzi.

swift
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)
    }
}

Kod tworzy poziomy ScrollView z LazyHStack. Ze 100 prostokątów jednocześnie wyświetlanych jest 2–3 (w zależności od szerokości ekranu i rozmiaru elementów). Podczas przewijania w lewo nowe elementy są ładowane leniwie. Wysokość kontenera jest stała (220pt), aby uniknąć nieskończonej wysokości w poziomym przewijaniu.

PinnedViews i sekcje w leniwych stosach

PinnedScrollableViews — opcja konfiguracji LazyVStack i LazyHStack, zarządzająca przypinaniem nagłówków i stopek sekcji podczas przewijania. Obsługiwane są dwie wartości: sectionHeaders (nagłówki przyklejają się do początku kontenera) i sectionFooters (stopki przyklejają się do końca).

Mechanizm pinned views działa tylko wewnątrz kontenera Section zagnieżdżonego w LazyVStack. Każda sekcja ma nagłówek i/lub stopkę, które automatycznie uzyskują zachowanie przyklejania. SwiftUI śledzi pozycję każdej sekcji względem granic ScrollView i przełącza widoczność przypiętego elementu przy przejściu między sekcjami.

Warto pamiętać: pinnedViews zwiększają złożoność obliczania układu, ponieważ SwiftUI musi stale przeliczać, który nagłówek jest aktualnie przypięty. Używaj pinnedViews tylko gdy funkcjonalność jest naprawdę potrzebna — dla prostych list bez sekcji lepiej pominąć ten parametr. Apple w swojej dokumentacji (Human Interface Guidelines, 2024) zaleca używanie przypiętych nagłówków dla indeksów alfabetycznych i grupowania według dat.

Wskazówki dotyczące optymalizacji leniwych stosów

Prawidłowe używanie identyfikatorów — najważniejszy czynnik wydajności LazyVStack. Każdy element w ForEach musi mieć stabilny unikalny id. Używanie \.self z typami prymitywnymi (Int, String) jest dopuszczalne, ale dla modeli danych zawsze implementuj protokół Identifiable. Niestabilne id (np. UUID generowane za każdym razem) zmuszają SwiftUI do odtwarzania wszystkich widoków przy każdej aktualizacji.

Unikaj ciężkich obliczeń wewnątrz body każdego elementu stosu. Jeśli element zawiera złożony układ lub przetwarzanie danych — wynieś logikę do osobnej struktury widoku z własnym leniwym ładowaniem. Używaj EquatableView do zapobiegania zbędnym przerysowaniom, gdy dane elementu się nie zmieniły.

Dla obrazów wewnątrz LazyVStack obowiązkowo stosuj asynchroniczne ładowanie (AsyncImage) lub buforowanie przez Kingfisher/Nuke. Każdy element pojawiający się na ekranie nie powinien synchronicznie ładować obrazu — spowoduje to zacięcia przewijania (jank). Według danych z WWDC Session 10031, optymalny rozmiar bufora prefetching to 3–5 ekranów w przód i w tył od bieżącej pozycji.

Mierz wydajność przez Xcode Instruments z profilem SwiftUI. Zwracaj uwagę na metryki: liczbę ewaluacji body (body evaluations), alokacje (allocations) i częstotliwość klatek (FPS). Docelowe wartości: FPS > 55 podczas przewijania, czas renderowania jednego elementu < 1 ms.

Często zadawane pytania

Jaka jest różnica między LazyVStack a List w SwiftUI?

List zapewnia wbudowane funkcje: edycję przez przeciągnięcie (swipeActions), usuwanie przez .onDelete, przenoszenie przez .onMove, styl grupowania .insetGrouped. LazyVStack to narzędzie niższego poziomu bez wbudowanej obsługi gestów edycji. List używa LazyVStack wewnętrznie, ale dodaje natywny styl tabeli iOS. Jeśli potrzebujesz niestandardowego wyglądu komórki i nie potrzebujesz wbudowanej edycji — wybierz LazyVStack. Jeśli potrzebujesz swipeActions, .onDelete i pracy z @FetchRequest — użyj List.

Dlaczego LazyVStack tworzy elementy, które nie są widoczne na ekranie?

Leniwe stosy używają prefetchingu — SwiftUI tworzy elementy z niewielkim wyprzedzeniem (prefetch buffer), aby przewijanie było płynne. Rozmiar bufora jest automatycznie dostosowywany do prędkości przewijania i wydajności urządzenia. Według danych z profilowania Apple, prefetch buffer wynosi zwykle 1–3 ekrany w kierunku przewijania. Jeśli widzisz, że tworzy się zbyt wiele niewidocznych elementów, sprawdź czy nie masz identyfikatorów generowanych za każdym razem lub ciężkich obliczeń w inicjalizatorze widoku.

Czy można zagnieżdżać LazyVStack w VStack lub odwrotnie?

Tak, ale z ograniczeniami. Zagnieżdżanie LazyVStack w VStack nie ma sensu — zewnętrzny VStack utworzy wszystkie elementy wewnętrznego LazyVStack natychmiast, anulując leniwe ładowanie. Zagnieżdżanie VStack w LazyVStack jest dozwolone i nie psuje mechanizmu leniwego. Zagnieżdżanie LazyVStack w innym LazyVStack jest dopuszczalne dla zagnieżdżonych sekcji, ale kontroluj wydajność: każdy poziom dodaje narzut na śledzenie geometrii.

Jak dodać separator między elementami LazyVStack?

SwiftUI nie zapewnia wbudowanych separatorów dla LazyVStack. Dodaj je ręcznie: umieść Divider() po każdym elemencie w ForEach, albo użyj modyfikatora .overlay(Divider(), alignment: .bottom) na każdym elemencie. Dla niestandardowych separatorów rysuj Rectangle().frame(height: 1).foregroundColor(.gray.opacity(0.3)).

Podsumowanie

  • LazyVStack — pionowy leniwy stos SwiftUI do efektywnego renderowania dużych list z tworzeniem elementów przy wejściu w obszar widoczności.
  • LazyHStack — poziomy odpowiednik dla karuzel i poziomych list z tym samym mechanizmem leniwego ładowania.
  • Różnica od VStack — VStack tworzy wszystkie elementy natychmiast; leniwe stosy tylko widoczne, oszczędzając do 50× pamięci na dużych zbiorach.
  • pinnedViews — przypinanie nagłówków sekcji przez Section { header: }.sectionHeaders dla efektu „przyklejania".
  • Identyfikacja przez id — obowiązkowy warunek: ForEach wymaga unikalnego stabilnego id dla każdego elementu.
  • Wydajność — prefetch buffer 1–3 ekrany, EquatableView do zapobiegania zbędnym przerysowaniom, asynchroniczne ładowanie obrazów.
  • Wybór narzędzia — LazyVStack dla niestandardowych projektów, List dla wbudowanej edycji i swipeActions.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również