Μάθετε τι είναι τα 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 αποθηκεύει δεδομένα πηγής (π.χ., πίνακα μοντέλων) και δημιουργεί στιγμιότυπα προβολής ακριβώς πριν από την απόδοση στην οθόνη. Κατά την κύλιση, οι στοίβες επαναχρησιμοποιούν ήδη δημιουργημένες προβολές, αποφεύγοντας νέες εκχωρήσεις — αυτό μειώνει το φορτίο στον εκχωρητή μνήμης και τον συλλέκτη απορριμμάτων 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 — η στρατηγική δημιουργίας θυγατρικών στοιχείων. Το 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 παραμένει η καλύτερη επιλογή για στατικές διεπαφές: οθόνη προφίλ, φόρμα σύνδεσης, κάρτα προϊόντος — όπου ο αριθμός στοιχείων είναι γνωστός και δεν υπερβαίνει τα 10–15. Το VStack λειτουργεί ταχύτερα κατά την πρώτη απόδοση τέτοιου αριθμού στοιχείων, καθώς δεν σπαταλά πόρους σε παρακολούθηση γεωμετρίας και τεμπέλικη φόρτωση. Επιπλέον, το VStack λειτουργεί σωστά εκτός ScrollView (π.χ., εντός ZStack ή Group), ενώ το LazyVStack χωρίς ScrollView χάνει το νόημά του.
Οι τεμπέλικες στοίβες είναι βέλτιστες για σενάρια με μεγάλο ή απρόβλεπτο αριθμό στοιχείων: ροές κοινωνικών δικτύων, κατάλογοι προϊόντων, λίστες συνομιλιών, βιβλιοθήκες αρχείων πολυμέσων, αρχεία καταγραφής συμβάντων, πίνακες διαχείρισης με χιλιάδες εγγραφές.
Συγκεκριμένες περιπτώσεις χρήσης: λίστα μηνυμάτων σε messenger (δεκάδες χιλιάδες μηνύματα), καρουζέλ εικόνων σε εφαρμογή γκαλερί, ροή ειδήσεων με ατελείωτη φόρτωση, λίστα παραγγελιών σε ηλεκτρονικό κατάστημα. Το LazyHStack είναι ιδιαίτερα χρήσιμο για οριζόντια καρουζέλ — όπως Stories στο Instagram ή διαφημιστικά banners.
Αντενδείξεις: διεπαφές με κινούμενα σχέδια εμφάνισης στοιχείων (οι τεμπέλικες στοίβες δεν υποστηρίζουν μεταβάσεις μεταξύ καταστάσεων διαγραφής στοιχείων χωρίς πρόσθετη λογική), περιπτώσεις όπου όλα τα στοιχεία πρέπει να είναι ορατά ταυτόχρονα (μικρή λίστα πλαισίων ελέγχου) και όταν χρειάζεστε ακριβή έλεγχο επαναχρησιμοποίησης κελιών (σε αυτήν την περίπτωση, το 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, το βέλτιστο μέγεθος buffer για 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) για ομαλή κύλιση. Το μέγεθος 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. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης