UITableView — คอมโพเนนต์หลักของ UIKit สำหรับแสดงรายการแนวตั้งใน iOS แต่ละแถวของตารางคือเซลล์ UITableViewCell ที่มีสไตล์ที่กำหนดไว้ล่วงหน้า เราอธิบายพื้นฐานของ UITableView: DataSource, delegate, การใช้เซลล์ซ้ำ และประสิทธิภาพของตารางในแอปพลิเคชัน iOS ตามข้อมูลของ Apple (Human Interface Guidelines, 2026) UITableView ยังคงเป็นคอมโพเนนต์ UI ที่ใช้มากที่สุดใน iOS — 85% ของแอปใน 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 ภายในเซลล์ การปรับขนาดอัตโนมัติจะลดประสิทธิภาพสำหรับเซลล์ที่ซับซ้อน — ใช้ความสูงคงที่ (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 ที่กำหนดเอง ข้อดี: รองรับ Dynamic Type โหมดมืด และ VoiceOver ในตัว Apple (WWDC 2024) แนะนำ contentConfiguration สำหรับ UITableView ใหม่ทั้งหมด
UITableViewDataSource — โปรโตคอลที่ให้ข้อมูลสำหรับตาราง วิธีการที่จำเป็น: tableView(_:numberOfRowsInSection:) และ tableView(_:cellForRowAt:) หากไม่มีวิธีการเหล่านี้ ตารางจะไม่สามารถแสดงแถวใดได้เลย UITableViewDelegate — โปรโตคอลสำหรับจัดการลักษณะและพฤติกรรม: ความสูงของแถว การเลือก การแก้ไข การดำเนินการตามบริบท
// ตัวอย่างที่มีหลายส่วนและการแก้ไข
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)
}
// Swipe-to-delete
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 โดยตรง — เร็วกว่า heightForRowAt ถึง 10 เท่า สำหรับเซลล์ปรับขนาดอัตโนมัติ ให้ตั้งค่า estimatedRowHeight และ automaticDimension — ตารางจะเรียก heightForRowAt เฉพาะแถวที่มองเห็นและใช้ estimatedRowHeight สำหรับแถวที่เหลือ
คิวรีไซเคิล — กลุ่มของเซลล์ 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
// ... constraints ...
}
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() — วิธีการที่เรียกก่อนวางเซลล์ในคิวรีไซเคิล ให้รีเซ็ตสถานะทั้งหมดของเซลล์: ข้อความ รูปภาพ แอนิเมชัน การดำเนินการโหลด หากไม่มี 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 | มุมมองที่กำหนดเองสำหรับส่วนหัว | UIView? |
| heightForHeaderInSection | ความสูงของส่วนหัว | CGFloat |
การทำดัชนี: สำหรับการนำทางที่รวดเร็วระหว่างส่วนต่างๆ ให้เพิ่มชื่อดัชนี — อาร์เรย์ของสตริงที่แสดงทางด้านขวา ใช้ sectionIndexTitles(for:) — ส่งคืนอาร์เรย์ของตัวอักษรแรกของแต่ละส่วน สำหรับ 26+ ส่วน สิ่งนี้ช่วยปรับปรุง UX อย่างมาก ใน iOS 15+ ตารางรองรับ sectionHeaderTopPadding — ระยะห่างระหว่างแถบสถานะและส่วนหัวแรก ซึ่งสามารถรีเซ็ตเป็น 0 สำหรับเลย์เอาต์ที่แน่นหนา
ประสิทธิภาพ ของ UITableView มีความสำคัญสำหรับแอปที่มีรายการยาว ปัญหาทั่วไป: การเลื่อนช้า (jank) เนื่องจากการดำเนินการหนักใน cellForRowAt การเรียก heightForRowAt บ่อยครั้ง การขาด prefetching Apple (WWDC 2025) เน้นย้ำแนวทางปฏิบัติสำคัญห้าประการสำหรับตารางที่มีมากกว่า 1,000 รายการ
// การเพิ่มประสิทธิภาพด้วย 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 สำหรับตารางที่มีรูปภาพ (อวตาร, ตัวอย่าง, ไอคอนแอป)
คำถามที่พบบ่อย
ตรวจสอบ: (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 รายการ สำหรับเซลล์ที่มีความสูงที่กำหนดเอง (เนื้อหาต่างกัน) ให้ใช้การปรับขนาดอัตโนมัติ: estimatedRowHeight + UITableView.automaticDimension
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ