UICollectionView เป็นคอมโพเนนต์ UIKit ที่ทรงพลังสำหรับ iOS ที่จัดการการแสดงคอลเลกชันของวัตถุในรูปแบบกริด รายการ และเลย์เอาต์ที่กำหนดเอง ซึ่งแตกต่างจาก UITableView ตรงที่ UICollectionView ไม่จำกัดอยู่เพียงคอลัมน์เดียว: รองรับการวางตำแหน่งองค์ประกอบตามต้องการผ่าน UICollectionViewLayout เราอธิบายแก่นแท้ของ UICollectionView วิธีที่ FlowLayout และ CompositionalLayout ทำงาน และวิธีสร้างเซลล์ที่กำหนดเองใน Swift ที่ IT Sectr เราใช้ UICollectionView สำหรับแกลเลอรี แคตตาล็อก และกระดานประกาศ สำหรับการเปรียบเทียบกับตาราง อ่าน บทความเกี่ยวกับ UITableView
ประเด็นสำคัญ
UICollectionView เป็นคลาสจากเฟรมเวิร์ก UIKit ที่เปิดตัวใน iOS 6 (2012) มันแสดงคอลเลกชันที่เรียงลำดับของเซลล์ (UICollectionViewCell) ในเลย์เอาต์ที่ยืดหยุ่นซึ่งจัดการโดยวัตถุ UICollectionViewLayout UICollectionView แก้ปัญหาเดียวกันกับ UITableView (รายการ) แต่มีการวางตำแหน่งองค์ประกอบตามต้องการ: กริด แครอเซล โมเสก น้ำตก
สถาปัตยกรรม UICollectionView สร้างขึ้นบนรูปแบบ MVC: ข้อมูลใน DataSource เลย์เอาต์ใน UICollectionViewLayout พฤติกรรมใน UICollectionViewDelegate การแยกนี้ช่วยให้สามารถเปลี่ยนเลย์เอาต์ได้โดยไม่ต้องเปลี่ยนโค้ดข้อมูล ตามข้อมูลของ Apple (WWDC 2025) 72% ของแอป iOS ใน 100 อันดับแรกของ App Store ใช้ UICollectionView สำหรับการแสดงเนื้อหา CollectionView รองรับเลย์เอาต์หลายคอลัมน์ตั้งแต่ iOS 6 และตั้งแต่ iOS 13 ก็ได้รับ CompositionalLayout สำหรับส่วนที่ซับซ้อน
UICollectionView ใช้พูลของเซลล์ที่ใช้ซ้ำได้ (reuse queue) คล้ายกับ UITableView เมธอด dequeueReusableCell(withReuseIdentifier:for:) จะคืนค่าเซลล์จากพูลหรือสร้างใหม่หากพูลว่างเปล่า ซึ่งแตกต่างจาก UITableView ตรงที่ UICollectionViewCell มี contentView ในตัวซึ่งคุณควรเพิ่มซับวิวทั้งหมด — อย่าเพิ่มลงในเซลล์โดยตรง เนื่องจาก contentView รับผิดชอบอนิเมชันการเลือกและการแก้ไขที่ถูกต้อง
UICollectionViewFlowLayout — เลย์เอาต์มาตรฐานที่ทำงานตั้งแต่ iOS 6 มันจัดเรียงองค์ประกอบเป็นแถว (flow) โดยมีความสามารถในการกำหนดค่าขนาดเซลล์ ทิศทางการเลื่อน ระยะห่างขั้นต่ำ และส่วนต่างๆ UICollectionViewCompositionalLayout — เลย์เอาต์ที่ทันสมัย (iOS 13+) ที่อธิบายเลย์เอาต์ผ่านการรวมกันของบล็อก: กลุ่ม → ส่วน → เลย์เอาต์ โดยแต่ละกลุ่มสามารถเป็นแนวนอน แนวตั้ง หรือกำหนดเอง
| พารามิเตอร์ | UICollectionViewFlowLayout | UICollectionViewCompositionalLayout |
|---|---|---|
| เวอร์ชันขั้นต่ำ | iOS 6 | iOS 13 |
| เลย์เอาต์ที่ซับซ้อน | เฉพาะกริดหรือรายการธรรมดา | ส่วนที่มีเลย์เอาต์ต่างกัน กลุ่ม แครอเซล |
| ความสามารถในการปรับเปลี่ยน | ผ่านขนาดดีลิเกต | NSCollectionLayoutDimension.fractionalWidth/Height |
| ประสิทธิภาพ | เรนเดอร์องค์ประกอบที่มองเห็นทั้งหมด | การสร้างส่วนแบบขี้เกียจ |
| รองรับ iPad | ต้องการการปรับเปลี่ยนด้วยตนเอง | กลุ่มแบบปรับได้ผ่าน fractionalWidth |
| ความซับซ้อนในการนำไปใช้ | ต่ำ (5–10 บรรทัด) | ปานกลาง (15–30 บรรทัด) |
// UICollectionView พื้นฐานกับ FlowLayout
class ViewController: UIViewController {
private var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumInteritemSpacing = 8
layout.minimumLineSpacing = 12
layout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
layout.itemSize = CGSize(width: view.frame.width - 32, height: 100)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "cell")
collectionView.dataSource = self
view.addSubview(collectionView)
}
}
// DataSource
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int { 20 }
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
as! CustomCell
cell.configure(with: "Item \(indexPath.row)")
return cell
}
}
คำแนะนำของ Apple (WWDC 2025): ใช้ CompositionalLayout สำหรับโปรเจกต์ใหม่ มันให้ความสามารถมากขึ้นสำหรับเลย์เอาต์แบบปรับได้ ทำงานได้ดีกว่ากับ UIKit และ SwiftUI ผ่าน UICollectionView.Representable และรองรับส่วนหัวของส่วนที่มีขนาดอัตโนมัติ เก็บ FlowLayout ไว้สำหรับหน้าจอธรรมดาที่มีกริดของเซลล์ที่เหมือนกันและเวอร์ชันขั้นต่ำ iOS 12 และต่ำกว่า
UICollectionViewCell — คลาสพื้นฐานสำหรับเซลล์ UICollectionView แต่ละเซลล์ประกอบด้วย contentView (คอนเทนเนอร์หลัก), backgroundView (พื้นหลังเริ่มต้น), selectedBackgroundView (พื้นหลังเมื่อเลือก) กำหนดลักษณะของเซลล์ภายใน contentView — อย่าเพิ่มซับวิวลงใน UICollectionViewCell โดยตรง
// เซลล์ที่กำหนดเองพร้อมการกำหนดค่า
class ProductCell: UICollectionViewCell {
private let imageView = UIImageView()
private let titleLabel = UILabel()
private let priceLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
contentView.addSubview(imageView)
contentView.addSubview(titleLabel)
contentView.addSubview(priceLabel)
// ข้อจำกัดของ Auto Layout
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
priceLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8)
])
}
func configure(with product: Product) {
imageView.image = UIImage(named: product.imageName)
titleLabel.text = product.title
priceLabel.text = product.price
}
}
UICollectionViewDelegate จัดเตรียมเมธอดสำหรับจัดการการเลือก การเน้น เมนูบริบท และการจัดลำดับใหม่ ซึ่งแตกต่างจาก UITableView ตรงที่ UICollectionViewDelegate อนุญาตให้ควบคุมไม่เพียงแต่การแตะองค์ประกอบเท่านั้น แต่ยังรวมถึงการกดค้าง (เมนูบริบทผ่าน UIContextMenuInteraction) ในการจัดการการแตะ ให้ใช้ collectionView(_:didSelectItemAt:) — มันจะถูกเรียกเมื่ออนิเมชันการเลือกเสร็จสมบูรณ์
UICollectionViewDiffableDataSource — ตัวแทนที่ปลอดภัยชนิดสำหรับ UICollectionViewDataSource ที่เปิดตัวใน iOS 13 แทนที่จะจัดการเซลล์ด้วยตนเองผ่าน cellForItemAt DiffableDataSource ใช้ NSDiffableDataSourceSnapshot — ภาพรวมสถานะข้อมูลที่คำนวณความแตกต่างระหว่างสถานะเก่าและใหม่โดยอัตโนมัติและทำให้การเปลี่ยนแปลงเป็นอนิเมชัน
// DiffableDataSource พร้อมส่วน
enum Section {
case featured, recommended, categories
}
class ModernDataSource: UIViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Product>!
override func viewDidLoad() {
super.viewDidLoad()
configureDataSource()
applyInitialSnapshot()
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<ProductCell, Product> { cell, _, product in
cell.configure(with: product)
}
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
collectionView, indexPath, product in
collectionView.dequeueConfiguredReusableCell(
using: cellRegistration, for: indexPath, item: product
)
}
}
private func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Product>()
snapshot.appendSections([.featured, .recommended, .categories])
snapshot.appendItems(featuredProducts, toSection: .featured)
snapshot.appendItems(recommendedProducts, toSection: .recommended)
snapshot.appendItems(categories, toSection: .categories)
dataSource.apply(snapshot, animatingDifferences: true)
}
// อัปเดตด้วยอนิเมชันอัตโนมัติ
func updateProducts(_ products: [Product]) {
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers)
snapshot.appendItems(products)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
ข้อดีของ DiffableDataSource: (1) อนิเมชันอัตโนมัติ — insert, delete, reload เกิดขึ้นพร้อมอนิเมชันเริ่มต้น; (2) ความปลอดภัยของชนิด — ส่วนและรายการถูกกำหนดชนิด ข้อผิดพลาดตอนคอมไพล์แทนที่จะเกิดครา�
UICollectionViewLayout — คลาสนามธรรมที่ควบคุมตำแหน่งขององค์ประกอบคอลเลกชันอย่างสมบูรณ์ หาก FlowLayout และ CompositionalLayout ไม่เหมาะสม (เช่น เลย์เอาต์วงกลม เกลียว แผนภาพ) ให้สร้างคลาสย่อยของ UICollectionViewLayout และแทนที่เมธอดการเตรียมเลย์เอาต์
// เลย์เอาต์วงกลมที่กำหนดเอง
class CircularLayout: UICollectionViewLayout {
private var attributesCache: [UICollectionViewLayoutAttributes] = []
private let radius: CGFloat = 120
override func prepare() {
super.prepare()
attributesCache.removeAll()
guard let collectionView else { return }
let count = collectionView.numberOfItems(inSection: 0)
let center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
for i in 0..<count {
let indexPath = IndexPath(item: i, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let angle = (CGFloat(i) / CGFloat(count)) * 2 * .pi
attributes.center = CGPoint(
x: center.x + radius * cos(angle),
y: center.y + radius * sin(angle)
)
attributes.size = CGSize(width: 60, height: 60)
attributesCache.append(attributes)
}
}
override var collectionViewContentSize: CGSize {
collectionView?.bounds.size ?? .zero
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
attributesCache.filter { $0.frame.intersects(rect) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
attributesCache[indexPath.item]
}
}
ประสิทธิภาพ: UICollectionViewLayout ที่กำหนดเองควรแคชแอตทริบิวต์ใน prepare() และส่งคืนใน O(1) ใน layoutAttributesForItem(at:) สำหรับคอลเลกชันที่มีมากกว่า 1000 องค์ประกอบ ให้ใช้การอัปเดตแคชแบบเพิ่มหน่วยผ่าน shouldInvalidateLayout(forBoundsChange:) — ซึ่งจะหลีกเลี่ยงการคำนวณเลย์เอาต์ทั้งหมดใหม่เมื่อเลื่อน Apple (WWDC 2023) แนะนำ CompositionalLayout แทนเลย์เอาต์ที่กำหนดเอง: ครอบคลุม 95% ของสถานการณ์โดยไม่ต้องเขียนเลย์เอาต์จากศูนย์
UITableView และ UICollectionView เป็นคอมโพเนนต์ UIKit หลักสองตัวสำหรับแสดงรายการ UITableView ง่ายกว่าและออกแบบมาสำหรับรายการคอลัมน์เดียว UICollectionView ยืดหยุ่นกว่าและรองรับเลย์เอาต์ใด ๆ การเลือกระหว่างพวกเขาขึ้นอยู่กับโครงสร้างข้อมูลที่มองเห็นและการปรับแต่งที่ต้องการ
| พารามิเตอร์ | UITableView | UICollectionView |
|---|---|---|
| เลย์เอาต์ | เฉพาะรายการแนวตั้ง | รายการ กริด น้ำตก แครอเซล กำหนดเอง |
| ความซับซ้อน | ต่ำ (DataSource ธรรมดา) | ปานกลาง (Layout + DataSource + Delegate) |
| เซลล์เริ่มต้น | 4 สไตล์ (basic, subtitle, value1, value2) | ว่างเท่านั้น (กำหนดเองผ่าน contentView) |
| โหมดแก้ไข | ในตัว (delete, move, insert) | ต้องการการนำไปใช้ด้วยตนเอง |
| Header/Footer | ในตัว (viewForHeaderInSection) | ผ่าน supplementary views |
| ประสิทธิภาพ | สูงสำหรับรายการ | สูงสำหรับเลย์เอาต์ใด ๆ |
| iOS 6+ | พร้อมใช้งานตั้งแต่ iOS 2 | พร้อมใช้งานตั้งแต่ iOS 6 |
กฎปฏิบัติ: ใช้ UITableView หากหน้าจอเป็นรายการแถวธรรมดา (การตั้งค่า รายชื่อ เมนู) ใช้ UICollectionView หากหน้าจอต้องการกริด การเลื่อนแนวนอน แครอเซล ขนาดเซลล์ต่างกัน หรือการจัดกลุ่มที่ซับซ้อน ตั้งแต่ iOS 14 เป็นต้นมา UITableView ก็รองรับ CompositionalLayout ผ่าน UICollectionLayoutListConfiguration ซึ่งช่วยให้รวมข้อดีของทั้งสองคอมโพเนนต์
คำถามที่พบบ่อย
ตั้งค่า scrollDirection = .horizontal ใน UICollectionViewFlowLayout สำหรับ CompositionalLayout ให้ใช้ NSCollectionLayoutSize ด้วย fractionalWidth สำหรับความกว้างกลุ่มที่ปรับได้ การเลื่อนแนวนอนเหมาะสำหรับแครอเซล แกลเลอรี และหมวดหมู่ สำหรับการเลื่อนแบบหน้า ให้ตั้งค่า collectionView.isPagingEnabled = true หรือใช้ UICollectionViewFlowLayout ด้วย itemSize = bounds.width
ตรวจสอบ: (1) เซลล์ถูกลงทะเบียนผ่าน register(_:forCellWithReuseIdentifier:), (2) DataSource ส่งคืนจำนวนรายการ > 0, (3) เซลล์ถูก dequeue ด้วย reuseIdentifier ที่ถูกต้อง, (4) itemSize ไม่เป็นศูนย์ (สำหรับ FlowLayout) ข้อผิดพลาดทั่วไปคือ itemSize = CGSize.zero ซึ่งทำให้เซลล์มีความสูงเป็นศูนย์ ตั้งค่า itemSize อย่างชัดเจนหรือใช้ดีลิเกต collectionView(_:layout:sizeForItemAt:)
สำหรับ FlowLayout: layout.minimumInteritemSpacing (ระหว่างรายการในแถว) และ layout.minimumLineSpacing (ระหว่างแถว) สำหรับ CompositionalLayout: NSCollectionLayoutGroup.interItemSpacing และ contentInsets บนกลุ่ม/ส่วน sectionInset กำหนดขอบเขตภายนอกของส่วน ใช้ sectionInsetReference = .fromContentInset สำหรับการจัดการ Safe Area ที่ถูกต้อง
ใช้ performBatchUpdates สำหรับอนิเมชันกลุ่ม: collectionView.performBatchUpdates { insertItems, deleteItems, reloadItems } สำหรับการอัปเดตที่ปลอดภัยและมีอนิเมชัน ให้ใช้ UICollectionViewDiffableDataSource: เรียก dataSource.apply(snapshot, animatingDifferences: true) — การเปลี่ยนแปลงทั้งหมดจะถูกคำนวณและนำไปใช้โดยอัตโนมัติโดยไม่ต้อง insert/delete ด้วยตนเอง
ใน SwiftUI สิ่งที่เทียบเท่ากับ UICollectionView คือ LazyVGrid และ LazyHGrid (iOS 14+) SwiftUI Grid เขียนง่ายกว่า (ไวยากรณ์แบบประกาศ) แต่ด้อยกว่า UICollectionView ในประสิทธิภาพสำหรับ 500+ องค์ประกอบและการปรับแต่งเลย์เอาต์ สำหรับคอลเลกชันที่ซับซ้อน ให้ใช้ UICollectionView ผ่าน UIViewRepresentable ในโปรเจกต์ IT Sectr เราเลือก UICollectionView สำหรับแคตตาล็อกและแกลเลอรี และ SwiftUI Grid สำหรับหน้าจอธรรมดา
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ