LazyVStack: τι είναι οι κάθετες και οριζόντιες τεμπέλικες στοίβες στο SwiftUI

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-02-24 Χρόνος ανάγνωσης: 10 λεπ

Μάθετε τι είναι τα LazyVStack και LazyHStack στο SwiftUI — τεμπέλικες στοίβες για αποδοτική απόδοση κυλιόμενων λιστών, πλεγμάτων και καρουζέλ σε iOS, macOS, watchOS και tvOS. Σε αντίθεση με τις συνηθισμένες VStack και HStack, οι τεμπέλικες στοίβες δημιουργούν στοιχεία μόνο όταν εμφανίζονται στην ορατή περιοχή, μειώνοντας κρίσιμα την κατανάλωση μνήμης κατά την εργασία με μεγάλα σύνολα δεδομένων. Η αρχιτεκτονική των τεμπέλικων στοιβών βασίζεται στο πρωτόκολλο Layout και είναι ενσωματωμένη με αναγνώριση μέσω ForEach και ScrollView.

Κύρια σημεία

  • Τεμπέλικη φόρτωση — Τα LazyVStack και LazyHStack δημιουργούν θυγατρικές προβολές μόνο όταν εισέρχονται στην ορατή περιοχή του ScrollView, εξοικονομώντας μνήμη και βελτιώνοντας το FPS κατά την κύλιση.
  • Αναγνώριση μέσω id — για σωστή λειτουργία με τεμπέλικα κοντέινερ, κάθε στοιχείο στο ForEach πρέπει να έχει μοναδικό αναγνωριστικό (id: \.self ή πρωτόκολλο Identifiable).
  • pinnedViews — η παράμετρος επιτρέπει το καρφίτσωμα κεφαλίδων ενοτήτων (sectionHeaders, sectionFooters) στο πάνω ή κάτω μέρος της οθόνης κατά την κύλιση, παρόμοια με τα UITableView section headers.
  • spacing και alignment — και οι δύο στοίβες δέχονται προσαρμοσμένο διάστημα μεταξύ στοιχείων και στοίχιση (leading, center, trailing για VStack; top, center, bottom για HStack).
  • Φώλιασμα με GridItem — Τα LazyVGrid και LazyHGrid είναι χτισμένα στην ίδια αρχή τεμπέλικης φόρτωσης, αλλά με υποστήριξη στηλών και γραμμών μέσω GridItem.

Τι είναι τα LazyVStack και LazyHStack;

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 αποθηκεύει δεδομένα πηγής (π.χ., πίνακα μοντέλων) και δημιουργεί στιγμιότυπα προβολής ακριβώς πριν από την απόδοση στην οθόνη. Κατά την κύλιση, οι στοίβες επαναχρησιμοποιούν ήδη δημιουργημένες προβολές, αποφεύγοντας νέες εκχωρήσεις — αυτό μειώνει το φορτίο στον εκχωρητή μνήμης και τον συλλέκτη απορριμμάτων Swift.

Για εργασία με τεμπέλικες στοίβες, πρέπει να τις τοποθετήσετε μέσα σε ScrollView — χωρίς κύλιση, τα στοιχεία που υπερβαίνουν τα όρια της οθόνης θα κοπούν απλά, όχι θα δημιουργηθούν τεμπέλικα.

Πώς λειτουργεί η τεμπέλικη φόρτωση

Ο μηχανισμός τεμπέλικης φόρτωσης στο LazyVStack βασίζεται στη γεωμετρία: Το SwiftUI παρακολουθεί τη θέση κάθε θυγατρικής προβολής σε σχέση με το κοντέινερ ScrollView. Όταν ένα στοιχείο διασχίζει το όριο της ορατής περιοχής (με ένα μικρό buffer λίγων σημείων), το σύστημα καλεί τον αρχικοποιητή του και αποδίδει το περιεχόμενο. Όταν το στοιχείο εγκαταλείπει την οθόνη, το 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: απόδοση και μνήμη

Η κύρια διαφορά μεταξύ LazyVStack και VStack — η στρατηγική δημιουργίας θυγατρικών στοιχείων. Το VStack (eager stack) υπολογίζει το μέγεθος και τη θέση όλων των θυγατρικών προβολών τη στιγμή της απόδοσης, καθιστώντας το ακατάλληλο για μεγάλες δυναμικές λίστες. Το 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 εξακολουθεί να χρειάζεται

Το VStack παραμένει η καλύτερη επιλογή για στατικές διεπαφές: οθόνη προφίλ, φόρμα σύνδεσης, κάρτα προϊόντος — όπου ο αριθμός στοιχείων είναι γνωστός και δεν υπερβαίνει τα 10–15. Το VStack λειτουργεί ταχύτερα κατά την πρώτη απόδοση τέτοιου αριθμού στοιχείων, καθώς δεν σπαταλά πόρους σε παρακολούθηση γεωμετρίας και τεμπέλικη φόρτωση. Επιπλέον, το VStack λειτουργεί σωστά εκτός ScrollView (π.χ., εντός ZStack ή Group), ενώ το LazyVStack χωρίς ScrollView χάνει το νόημά του.

Πότε να χρησιμοποιείτε LazyVStack και LazyHStack

Οι τεμπέλικες στοίβες είναι βέλτιστες για σενάρια με μεγάλο ή απρόβλεπτο αριθμό στοιχείων: ροές κοινωνικών δικτύων, κατάλογοι προϊόντων, λίστες συνομιλιών, βιβλιοθήκες αρχείων πολυμέσων, αρχεία καταγραφής συμβάντων, πίνακες διαχείρισης με χιλιάδες εγγραφές.

Συγκεκριμένες περιπτώσεις χρήσης: λίστα μηνυμάτων σε messenger (δεκάδες χιλιάδες μηνύματα), καρουζέλ εικόνων σε εφαρμογή γκαλερί, ροή ειδήσεων με ατελείωτη φόρτωση, λίστα παραγγελιών σε ηλεκτρονικό κατάστημα. Το LazyHStack είναι ιδιαίτερα χρήσιμο για οριζόντια καρουζέλ — όπως Stories στο Instagram ή διαφημιστικά banners.

Αντενδείξεις: διεπαφές με κινούμενα σχέδια εμφάνισης στοιχείων (οι τεμπέλικες στοίβες δεν υποστηρίζουν μεταβάσεις μεταξύ καταστάσεων διαγραφής στοιχείων χωρίς πρόσθετη λογική), περιπτώσεις όπου όλα τα στοιχεία πρέπει να είναι ορατά ταυτόχρονα (μικρή λίστα πλαισίων ελέγχου) και όταν χρειάζεστε ακριβή έλεγχο επαναχρησιμοποίησης κελιών (σε αυτήν την περίπτωση, το List ή Table μπορεί να είναι προτιμότερα).

Παραδείγματα κώδικα: λίστα, πλέγμα και ενότητες

Παράδειγμα 1: Απλή κάθετη λίστα με LazyVStack

Το βασικό παράδειγμα εμφανίζει 1000 στοιχεία με ελάχιστη κατανάλωση μνήμης. Βασικά στοιχεία: ScrollView ως κοντέινερ κύλισης, LazyVStack για τεμπέλικη φόρτωση, ForEach με αναγνωριστικό για επανάληψη δεδομένων.

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

Ο κώδικας δημιουργεί ένα ScrollView, μέσα στο οποίο τοποθετείται LazyVStack με διάστημα 8pt μεταξύ στοιχείων. Το ForEach διατρέχει τον πίνακα items και δημιουργεί Text για κάθε δείκτη. Χάρη στην τεμπέλικη φόρτωση, από 1000 στοιχεία ταυτόχρονα στη μνήμη βρίσκονται μόνο τα ορατά 10–12.

Παράδειγμα 2: Ενότητες με pinnedViews

Το παράδειγμα δείχνει ομαδοποίηση στοιχείων σε ενότητες με καρφιτσωμένες κεφαλίδες, όπως στις επαφές iOS. Η Section ορίζει κεφαλίδα και περιεχόμενο, τα pinnedViews: .sectionHeaders καρφιτσώνει την κεφαλίδα στην κορυφή της οθόνης κατά την κύλιση.

swift
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 ταυτόχρονα.

Παράδειγμα 3: Οριζόντιο καρουζέλ με LazyHStack

Το LazyHStack χρησιμοποιείται για οριζόντια κύλιση — καρουζέλ εικόνων, οριζόντιες λίστες κατηγοριών. Η παράμετρος alignment: .top στοιχίζει τα στοιχεία στο πάνω άκρο.

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

Ο κώδικας δημιουργεί ένα οριζόντιο ScrollView με LazyHStack. Από 100 ορθογώνια, ταυτόχρονα εμφανίζονται 2–3 (ανάλογα με το πλάτος οθόνης και το μέγεθος στοιχείων). Κατά την κύλιση προς τα αριστερά, νέα στοιχεία φορτώνονται τεμπέλικα. Το ύψος του κοντέινερ είναι σταθερό (220pt) για να αποφευχθεί το άπειρο ύψος στην οριζόντια κύλιση.

PinnedViews και ενότητες σε τεμπέλικες στοίβες

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, το βέλτιστο μέγεθος buffer για prefetching είναι 3–5 οθόνες μπροστά και πίσω από την τρέχουσα θέση.

Μετρήστε την απόδοση μέσω Xcode Instruments με προφίλ SwiftUI. Δώστε προσοχή στις μετρήσεις: αριθμό αξιολογήσεων body, εκχωρήσεις και ρυθμό καρέ (FPS). Στόχοι: FPS > 55 κατά την κύλιση, χρόνος απόδοσης ενός στοιχείου < 1 ms.

Συχνές Ερωτήσεις

Ποια είναι η διαφορά μεταξύ LazyVStack και List στο SwiftUI;

Η List παρέχει ενσωματωμένες δυνατότητες: επεξεργασία με σύρσιμο (swipeActions), διαγραφή μέσω .onDelete, μετακίνηση μέσω .onMove, στυλ ομαδοποίησης .insetGrouped. Το LazyVStack είναι ένα εργαλείο χαμηλότερου επιπέδου χωρίς ενσωματωμένη υποστήριξη χειρονομιών επεξεργασίας. Η List χρησιμοποιεί LazyVStack εσωτερικά, αλλά προσθέτει εγγενές στυλ πίνακα iOS. Εάν χρειάζεστε προσαρμοσμένο σχέδιο κελιού και δεν χρειάζεστε ενσωματωμένη επεξεργασία — επιλέξτε LazyVStack. Εάν χρειάζεστε swipeActions, .onDelete και εργασία με @FetchRequest — χρησιμοποιήστε List.

Γιατί το LazyVStack δημιουργεί στοιχεία που δεν είναι ορατά στην οθόνη;

Οι τεμπέλικες στοίβες χρησιμοποιούν prefetching — Το SwiftUI δημιουργεί στοιχεία με μικρή προετοιμασία (prefetch buffer) για ομαλή κύλιση. Το μέγεθος buffer προσαρμόζεται αυτόματα στην ταχύτητα κύλισης και την απόδοση της συσκευής. Σύμφωνα με δεδομένα προφίλ της Apple, το prefetch buffer είναι συνήθως 1–3 οθόνες προς την κατεύθυνση κύλισης. Εάν βλέπετε ότι δημιουργούνται πάρα πολλά αόρατα στοιχεία, ελέγξτε αν έχετε αναγνωριστικά που δημιουργούνται κάθε φορά ή βαριούς υπολογισμούς στον αρχικοποιητή προβολής.

Μπορεί το LazyVStack να φωλιαστεί σε VStack ή αντίστροφα;

Ναι, αλλά με περιορισμούς. Το φώλιασμα LazyVStack σε VStack είναι άσκοπο — το εξωτερικό VStack θα δημιουργήσει αμέσως όλα τα στοιχεία του εσωτερικού LazyVStack, ακυρώνοντας την τεμπέλικη φόρτωση. Το φώλιασμα VStack σε LazyVStack επιτρέπεται και δεν χαλάει τον τεμπέλικο μηχανισμό. Το φώλιασμα LazyVStack σε άλλο LazyVStack είναι αποδεκτό για φωλιασμένες ενότητες, αλλά παρακολουθείτε την απόδοση: κάθε επίπεδο προσθέτει επιβάρυνση για παρακολούθηση γεωμετρίας.

Πώς προσθέτω διαχωριστικά μεταξύ στοιχείων LazyVStack;

Το SwiftUI δεν παρέχει ενσωματωμένα διαχωριστικά για LazyVStack. Προσθέστε τα χειροκίνητα: τοποθετήστε Divider() μετά από κάθε στοιχείο στο ForEach, ή χρησιμοποιήστε τον τροποποιητή .overlay(Divider(), alignment: .bottom) σε κάθε στοιχείο. Για προσαρμοσμένα διαχωριστικά, σχεδιάστε Rectangle().frame(height: 1).foregroundColor(.gray.opacity(0.3)).

Περίληψη

  • LazyVStack — κάθετη τεμπέλικη στοίβα SwiftUI για αποδοτική απόδοση μεγάλων λιστών με δημιουργία στοιχείων κατά την είσοδο στην ορατή περιοχή.
  • LazyHStack — οριζόντιο αντίστοιχο για καρουζέλ και οριζόντιες λίστες με τον ίδιο μηχανισμό τεμπέλικης φόρτωσης.
  • Διαφορά από VStack — το VStack δημιουργεί όλα τα στοιχεία αμέσως; οι τεμπέλικες στοίβες μόνο τα ορατά, εξοικονομώντας έως 50× μνήμη σε μεγάλα σύνολα.
  • pinnedViews — καρφίτσωμα κεφαλίδων ενοτήτων μέσω Section { header: }.sectionHeaders για εφέ «κόλλησης».
  • Αναγνώριση μέσω id — υποχρεωτική συνθήκη: το ForEach απαιτεί μοναδικό σταθερό id για κάθε στοιχείο.
  • Απόδοση — prefetch buffer 1–3 οθόνες, EquatableView για αποφυγή περιττών επανασχεδιάσεων, ασύγχρονη φόρτωση εικόνων.
  • Επιλογή εργαλείου — LazyVStack για προσαρμοσμένα σχέδια, List για ενσωματωμένη επεξεργασία και swipeActions.

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης