UITableView — το κύριο στοιχείο του UIKit για την εμφάνιση κάθετων λιστών στο iOS. Κάθε γραμμή του πίνακα είναι ένα κελί UITableViewCell με προκαθορισμένα στυλ. Εξηγούμε τα βασικά του UITableView: DataSource, εκπρόσωπος, επαναχρησιμοποίηση κελιών και απόδοση πινάκων σε εφαρμογές iOS. Σύμφωνα με την Apple (Human Interface Guidelines, 2026), το UITableView παραμένει το πιο χρησιμοποιημένο στοιχείο UI στο iOS — 85% των εφαρμογών top-100 του App Store περιέχουν τουλάχιστον έναν πίνακα. Για σύνθετες διατάξεις διαβάστε το άρθρο για το UICollectionView.
Κύρια σημεία
UITableView — είναι μια κλάση από το πλαίσιο UIKit που προορίζεται για την εμφάνιση μιας κυλιόμενης λίστας γραμμών σε μία στήλη. Κάθε γραμμή αντιπροσωπεύεται από ένα αντικείμενο UITableViewCell. Το UITableView εμφανίστηκε στο iPhone OS 2 (2008) και από τότε παραμένει το κύριο στοιχείο για λίστες στο iOS. Η αρχιτεκτονική του UITableView βασίζεται στο μοτίβο MVC: τα δεδομένα διαχειρίζονται από το DataSource, η εμφάνιση και συμπεριφορά από το Delegate, και η προβολή από τον ίδιο τον πίνακα.
Δύο στυλ πίνακα: .plain (συνεχής λίστα με προαιρετικές ενότητες) και .grouped (ομαδοποιημένες ενότητες με στρογγυλεμένες γωνίες και περιθώρια). Από το iOS 13 προστέθηκε το .insetGrouped — grouped-στυλ με περιθώρια από τις άκρες, που χρησιμοποιείται στις εφαρμογές Ρυθμίσεις και Υγεία. Η επιλογή στυλ επηρεάζει την προεπιλεγμένη εμφάνιση: οι plain-πίνακες καρφιτσώνουν την κεφαλίδα ενότητας στην κορυφή κατά την κύλιση (sticky header), grouped — όχι.
Από το iOS 8, το UITableView υποστηρίζει κελιά με αυτόματο μέγεθος (self-sizing cells). Για ενεργοποίηση ορίστε tableView.estimatedRowHeight (π.χ. 80) και tableView.rowHeight = UITableView.automaticDimension. Ο πίνακας υπολογίζει αυτόματα το ύψος γραμμής βάσει των περιορισμών Auto Layout μέσα στο κελί. Το self-sizing μειώνει την απόδοση για σύνθετα κελιά — χρησιμοποιήστε σταθερό ύψος (rowHeight) για πίνακες με 500+ στοιχεία.
UITableViewCell προσφέρει τέσσερα ενσωματωμένα στυλ που καλύπτουν το 80% των σεναρίων χωρίς να χρειάζεται να δημιουργήσετε προσαρμοσμένο κελί. Κάθε στυλ αποτελείται από έναν συνδυασμό textLabel, detailTextLabel και imageView. Η επιλογή στυλ ορίζεται κατά την αρχικοποίηση του κελιού μέσω init(style:reuseIdentifier:).
| Στυλ | textLabel | detailTextLabel | imageView | Παράδειγμα |
|---|---|---|---|---|
| .default (.basic) | Αριστερά, έντονο | Όχι | Προαιρετικό | Μενού, λίστα στοιχείων |
| .subtitle | Αριστερά, έντονο | Κάτω από textLabel, γκρι | Προαιρετικό | Επαφές, λίστες αναπαραγωγής |
| .value1 | Αριστερά, έντονο | Δεξιά, γκρι | Όχι | Ρυθμίσεις με τιμές |
| .value2 | Δεξιά, μπλε | Αριστερά, γκρι | Όχι | Τηλεφωνικός κατάλογος (στυλ iOS 6) |
// Δημιουργία πίνακα με προσαρμοσμένα κελιά
class ContactListController: UIViewController {
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.rowHeight = 60
view.addSubview(tableView)
// Auto Layout
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
extension ContactListController: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
contacts.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let contact = contacts[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = contact.name
content.secondaryText = contact.phone
content.image = UIImage(systemName: "person.circle")
cell.contentConfiguration = content
return cell
}
}
UIContentConfiguration (iOS 14+) — σύγχρονος τρόπος διαμόρφωσης κελιών μέσω contentConfiguration αντί για άμεση πρόσβαση σε textLabel/detailTextLabel. Χρησιμοποιήστε cell.defaultContentConfiguration() ή δημιουργήστε προσαρμοσμένο UIContentConfiguration. Πλεονεκτήματα: υποστήριξη δυναμικού τύπου, σκοτεινού θέματος και VoiceOver από το κουτί. Η Apple (WWDC 2024) συνιστά το contentConfiguration για όλα τα νέα UITableView.
UITableViewDataSource — πρωτόκολλο που παρέχει δεδομένα για τον πίνακα. Υποχρεωτικές μέθοδοι: tableView(_:numberOfRowsInSection:) και tableView(_:cellForRowAt:). Χωρίς αυτές, ο πίνακας δεν μπορεί να εμφανίσει καμία γραμμή. UITableViewDelegate — πρωτόκολλο για τη διαχείριση εμφάνισης και συμπεριφοράς: ύψος γραμμών, επιλογή, επεξεργασία, contextual actions.
// Παράδειγμα με πολλές ενότητες και επεξεργασία
extension ContactListController: UITableViewDelegate {
// Προσαρμοσμένο ύψος για κάθε γραμμή
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// Χειρισμός επιλογής
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let contact = contacts[indexPath.row]
showDetail(for: contact)
}
// Σύρσιμο για διαγραφή
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
-> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Διαγραφή") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
Απόδοση Delegate: οι μέθοδοι μεγέθους (heightForRowAt) καλούνται για κάθε ορατή γραμμή σε κάθε κύλιση. Για ομοιόμορφο ύψος ορίστε απευθείας rowHeight — αυτό είναι 10 φορές ταχύτερο από το heightForRowAt. Για κελιά self-sizing ορίστε estimatedRowHeight και automaticDimension — ο πίνακας θα καλέσει το heightForRowAt μόνο για ορατές γραμμές και για τις υπόλοιπες θα χρησιμοποιήσει το estimatedRowHeight.
Ουρά reuse — δεξαμενή επαναχρησιμοποιήσιμων κελιών UITableView. Όταν ένα κελί φεύγει από την οθόνη, δεν διαγράφεται αλλά τοποθετείται στην ουρά. Νέα κελιά δεν δημιουργούνται από την αρχή — το σύστημα καλεί dequeueReusableCell(withIdentifier:for:), η οποία επιστρέφει ένα κελί από την ουρά ή δημιουργεί νέο αν η ουρά είναι άδεια. Αυτός είναι ο βασικός μηχανισμός απόδοσης του UITableView.
// Προσαρμοσμένο κελί με προσωρινή αποθήκευση
class ContactCell: UITableViewCell {
private let avatarView = UIImageView()
private let nameLabel = UILabel()
private let subtitleLabel = UILabel()
override init(style: UITableViewCell.Style, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
contentView.addSubview(avatarView)
contentView.addSubview(nameLabel)
contentView.addSubview(subtitleLabel)
// Ρύθμιση περιορισμών Auto Layout
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... περιορισμοί ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// Ασύγχρονη φόρτωση άβαταρ
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// Εγγραφή και χρήση
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// Στο cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — μέθοδος που καλείται πριν από την τοποθέτηση του κελιού στην ουρά reuse. Επαναφέρετε ολόκληρη την κατάσταση του κελιού: κείμενο, εικόνες, κινούμενα σχέδια, φορτώσεις. Χωρίς prepareForReuse, ένα επαναχρησιμοποιημένο κελί μπορεί να εμφανίζει παλιά δεδομένα: η προηγούμενη εικόνα θα είναι ορατή για ένα κλάσμα του δευτερολέπτου μέχρι να φορτωθεί η νέα. Η Apple (WWDC 2023) συνιστά τον καθαρισμό ασύγχρονων λειτουργιών μέσω URLSessionTask.cancel() στο prepareForReuse.
Ενότητες στο UITableView ομαδοποιούν λογικά συνδεδεμένες γραμμές. Κάθε ενότητα μπορεί να έχει header (κεφαλίδα) και footer (υποσέλιδο). Ο αριθμός ενοτήτων καθορίζεται από τη μέθοδο numberOfSections(in:) (προεπιλογή 1). Για στυλ grouped και insetGrouped, οι ενότητες διαχωρίζονται οπτικά με περιθώρια και στρογγυλεμένες γωνίες.
| Μέθοδος DataSource | Περιγραφή | Τιμή επιστροφής |
|---|---|---|
| numberOfSections | Αριθμός ενοτήτων (προεπιλογή 1) | Int |
| numberOfRowsInSection | Αριθμός γραμμών στην ενότητα | Int |
| titleForHeaderInSection | Κείμενο κεφαλίδας ενότητας | String? |
| titleForFooterInSection | Κείμενο υποσέλιδου ενότητας | String? |
| viewForHeaderInSection | Προσαρμοσμένη προβολή για header | UIView? |
| heightForHeaderInSection | Ύψος header | CGFloat |
Ευρετηρίαση: για γρήγορη πλοήγηση μεταξύ ενοτήτων προσθέστε ένα ευρετήριο — μια συμβολοσειρά που εμφανίζεται στα δεξιά. Υλοποιήστε sectionIndexTitles(for:) — επιστρέφει μια συστοιχία από τα πρώτα γράμματα κάθε ενότητας. Για 26+ ενότητες αυτό βελτιώνει κρίσιμα το UX. Στο iOS 15+, οι πίνακες υποστηρίζουν sectionHeaderTopPadding — το διάστημα μεταξύ της γραμμής κατάστασης και της πρώτης κεφαλίδας που μπορεί να μηδενιστεί για συμπαγή διάταξη.
Απόδοση Το UITableView είναι κρίσιμο για εφαρμογές με μεγάλες λίστες. Κύρια προβλήματα: αργή κύλιση (jank) λόγω βαριών λειτουργιών στο cellForRowAt, συχνές κλήσεις heightForRowAt, έλλειψη prefetching. Η Apple (WWDC 2025) επισημαίνει πέντε βασικές πρακτικές για πίνακες με 1000+ στοιχεία.
// Βελτιστοποίηση με prefetching
extension ContactListController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView,
prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
ImageCache.shared.prefetch(url: contact.avatarUrl)
}
}
func tableView(_ tableView: UITableView,
cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
ImageCache.shared.cancelPrefetch(url: contact.avatarUrl)
}
}
}
// Χρήση DiffableDataSource για ομαλές ενημερώσεις
class ModernTableController: UIViewController {
private var dataSource: UITableViewDiffableDataSource<Section, Contact>!
func applyContacts(_ contacts: [Contact]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Contact>()
snapshot.appendSections([.main])
snapshot.appendItems(contacts)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
Πρακτικό αποτέλεσμα: η εφαρμογή αυτών των βελτιστοποιήσεων αυξάνει τα FPS κύλισης από 30 σε 60 σε συσκευές iPhone 12 και παλαιότερες (σύμφωνα με δοκιμές της Apple, 2025). Στα έργα IT Sectr, χρησιμοποιούμε το DiffableDataSource για όλους τους νέους πίνακες και προσθέτουμε prefetching για πίνακες με εικόνες (avatars, προεπισκοπήσεις, εικονίδια εφαρμογών).
Συχνές ερωτήσεις
Ελέγξτε: (1) contentSize > frame.size — ο πίνακας πρέπει να έχει περισσότερο περιεχόμενο από το ύψος του; (2) ο πίνακας δεν βρίσκεται μέσα σε άλλο UIScrollView που παρεμποδίζει χειρονομίες; (3) isScrollEnabled = true; (4) το numberOfRowsInSection επιστρέφει τον σωστό αριθμό. Τυπικό πρόβλημα — πίνακας με heightForRowAt = UITableView.automaticDimension χωρίς estimatedRowHeight, που οδηγεί σε ατελείωτο υπολογισμό ύψους.
Χρησιμοποιήστε performBatchUpdates ή DiffableDataSource αντί για reloadData(). Το reloadData() προκαλεί πλήρη επαναφόρτωση χωρίς κίνηση, δημιουργώντας οπτικό τρεμόπαιγμα. Για σημειακή ενημέρωση χρησιμοποιήστε: reloadRows(at:with:), insertRows, deleteRows. Όλες οι αλλαγές εντός του performBatchUpdates κινούνται ταυτόχρονα. Το DiffableDataSource το κάνει αυτόματα χωρίς χειροκίνητες κλήσεις.
UITableView — μονοστηλική κάθετη λίστα με προκαθορισμένα στυλ κελιών και ενσωματωμένη επεξεργασία. UICollectionView — ευέλικτη διάταξη (πλέγμα, λίστα, καρουζέλ, καταρράκτης) με αυθαίρετη τοποθέτηση στοιχείων. Επιλέξτε UITableView για απλές λίστες (ρυθμίσεις, επαφές, μηνύματα). Επιλέξτε UICollectionView για γκαλερί, καταλόγους, πίνακες και οποιεσδήποτε μη γραμμικές διατάξεις.
Ορίστε tableView.tableFooterView = UIView(frame: .zero). Από προεπιλογή, το UITableView εμφανίζει κενές γραμμές (διαχωριστικά) κάτω από το τελευταίο στοιχείο μέχρι το κάτω άκρο. Ο ορισμός κενού UIView στο tableFooterView τις αφαιρεί. Για grouped-στυλ αυτό δεν απαιτείται — ο πίνακας εμφανίζει μόνο τις πραγματικές γραμμές με στρογγυλεμένες γωνίες της τελευταίας ενότητας.
Ορίστε tableView.rowHeight = 80 και tableView.estimatedRowHeight = 0 ή παραλείψτε το estimatedRowHeight. Με σταθερό ύψος, το UITableView δεν καλεί το heightForRowAt, επιταχύνοντας την απόδοση 5–10 φορές για λίστες με 100+ στοιχεία. Για κελιά με προσαρμοσμένο ύψος (διαφορετικό περιεχόμενο) χρησιμοποιήστε self-sizing: estimatedRowHeight + UITableView.automaticDimension.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης