UICollectionView là một thành phần UIKit mạnh mẽ cho iOS quản lý việc hiển thị các bộ sưu tập đối tượng dưới dạng lưới, danh sách và bố cục tùy chỉnh. Không giống như UITableView, UICollectionView không bị giới hạn ở một cột: nó hỗ trợ định vị phần tử tùy ý thông qua UICollectionViewLayout. Chúng tôi giải thích bản chất của UICollectionView, cách FlowLayout và CompositionalLayout hoạt động, và cách tạo ô tùy chỉnh trong Swift. Tại IT Sectr, chúng tôi sử dụng UICollectionView cho thư viện ảnh, danh mục và bảng tin. Để so sánh với bảng, hãy đọc bài viết về UITableView.
Những điểm chính
UICollectionView là một lớp từ framework UIKit được giới thiệu trong iOS 6 (2012). Nó hiển thị một bộ sưu tập có thứ tự các ô (UICollectionViewCell) trong một bố cục linh hoạt được quản lý bởi đối tượng UICollectionViewLayout. UICollectionView giải quyết cùng vấn đề như UITableView (danh sách) nhưng với định vị phần tử tùy ý: lưới, băng chuyền, khảm, thác.
Kiến trúc UICollectionView được xây dựng trên mô hình MVC: dữ liệu trong DataSource, bố cục trong UICollectionViewLayout, hành vi trong UICollectionViewDelegate. Sự tách biệt này cho phép thay thế bố cục mà không thay đổi mã dữ liệu. Theo Apple (WWDC 2025), 72% ứng dụng iOS trong top 100 App Store sử dụng UICollectionView để hiển thị nội dung. CollectionView hỗ trợ bố cục nhiều cột từ iOS 6 và từ iOS 13 đã có CompositionalLayout cho các phần phức tạp.
UICollectionView sử dụng một nhóm các ô có thể tái sử dụng (reuse queue), tương tự như UITableView. Phương thức dequeueReusableCell(withReuseIdentifier:for:) trả về một ô từ nhóm hoặc tạo mới nếu nhóm trống. Không giống như UITableView, UICollectionViewCell có contentView tích hợp mà bạn nên thêm tất cả subview vào — không thêm trực tiếp vào ô, vì contentView chịu trách nhiệm cho hoạt ảnh chọn và chỉnh sửa chính xác.
UICollectionViewFlowLayout — bố cục tiêu chuẩn hoạt động từ iOS 6. Nó sắp xếp các phần tử theo hàng (flow) với khả năng cấu hình kích thước ô, hướng cuộn, khoảng cách tối thiểu và các phần. UICollectionViewCompositionalLayout — bố cục hiện đại (iOS 13+) mô tả bố cục thông qua sự kết hợp của các khối: nhóm → phần → bố cục, trong đó mỗi nhóm có thể ngang, dọc hoặc tùy chỉnh.
| Tham số | UICollectionViewFlowLayout | UICollectionViewCompositionalLayout |
|---|---|---|
| Phiên bản tối thiểu | iOS 6 | iOS 13 |
| Bố cục phức tạp | Chỉ lưới hoặc danh sách đơn giản | Phần với bố cục khác nhau, nhóm, băng chuyền |
| Khả năng thích ứng | Qua kích thước ủy quyền | NSCollectionLayoutDimension.fractionalWidth/Height |
| Hiệu suất | Kết xuất tất cả phần tử hiển thị | Tạo phần lười biếng |
| Hỗ trợ iPad | Yêu cầu thích ứng thủ công | Nhóm thích ứng qua fractionalWidth |
| Độ phức tạp triển khai | Thấp (5–10 dòng) | Trung bình (15–30 dòng) |
// UICollectionView cơ bản với 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
}
}
Khuyến nghị của Apple (WWDC 2025): sử dụng CompositionalLayout cho các dự án mới. Nó cung cấp nhiều khả năng hơn cho bố cục thích ứng, hoạt động tốt hơn với UIKit và SwiftUI qua UICollectionView.Representable, và hỗ trợ tiêu đề phần với kích thước tự động. Giữ FlowLayout cho các màn hình đơn giản với lưới ô giống hệt nhau và phiên bản tối thiểu iOS 12 trở xuống.
UICollectionViewCell — lớp cơ sở cho các ô UICollectionView. Mỗi ô chứa contentView (vùng chứa chính), backgroundView (nền mặc định), selectedBackgroundView (nền khi chọn). Cấu hình giao diện của ô bên trong contentView — không bao giờ thêm subview trực tiếp vào UICollectionViewCell.
// Ô tùy chỉnh với cấu hình
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)
// Ràng buộc 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 cung cấp các phương thức để xử lý lựa chọn, làm nổi bật, menu ngữ cảnh và sắp xếp lại. Không giống như UITableView, UICollectionViewDelegate cho phép kiểm soát không chỉ thao tác chạm vào phần tử mà còn cả nhấn giữ (menu ngữ cảnh qua UIContextMenuInteraction). Để xử lý thao tác chạm, hãy triển khai collectionView(_:didSelectItemAt:) — nó được gọi khi hoạt ảnh lựa chọn hoàn tất.
UICollectionViewDiffableDataSource — sự thay thế an toàn kiểu cho UICollectionViewDataSource được giới thiệu trong iOS 13. Thay vì quản lý ô thủ công qua cellForItemAt, DiffableDataSource sử dụng NSDiffableDataSourceSnapshot — ảnh chụp nhanh trạng thái dữ liệu tự động tính toán sự khác biệt giữa trạng thái cũ và mới và tạo hoạt ảnh cho các thay đổi.
// DiffableDataSource với các phần
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)
}
// Cập nhật với hoạt ảnh tự động
func updateProducts(_ products: [Product]) {
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers)
snapshot.appendItems(products)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
Lợi ích của DiffableDataSource: (1) hoạt ảnh tự động — insert, delete, reload xảy ra với hoạt ảnh mặc định; (2) an toàn kiểu — các phần và mục được định kiểu, lỗi biên dịch thay vì sự cố runtime; (3) Snapshot — ảnh chụp nhanh trạng thái có thể được lưu, hoàn tác, so sánh; (4) hiệu suất — DiffableDataSource sử dụng thuật toán Hellier để tính toán tập hợp thay đổi tối thiểu. Apple (WWDC 2024) khuyến nghị DiffableDataSource cho tất cả UICollectionView mới và chỉ giữ DataSource cũ cho iOS 12 trở về trước.
UICollectionViewLayout — một lớp trừu tượng kiểm soát hoàn toàn vị trí của các phần tử bộ sưu tập. Nếu FlowLayout và CompositionalLayout không phù hợp (ví dụ: bố cục hình tròn, xoắn ốc, biểu đồ), hãy tạo lớp con của UICollectionViewLayout và ghi đè các phương thức chuẩn bị bố cục.
// Bố cục hình tròn tùy chỉnh
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]
}
}
Hiệu suất: UICollectionViewLayout tùy chỉnh nên lưu trữ thuộc tính trong prepare() và trả về chúng trong O(1) tại layoutAttributesForItem(at:). Đối với bộ sưu tập có 1000+ phần tử, hãy sử dụng cập nhật bộ nhớ đệm gia tăng qua shouldInvalidateLayout(forBoundsChange:) — điều này tránh tính toán lại toàn bộ bố cục khi cuộn. Apple (WWDC 2023) khuyến nghị CompositionalLayout thay vì bố cục tùy chỉnh: nó bao phủ 95% tình huống mà không cần viết bố cục từ đầu.
UITableView và UICollectionView là hai thành phần UIKit chính để hiển thị danh sách. UITableView đơn giản hơn và được thiết kế cho danh sách một cột. UICollectionView linh hoạt hơn và hỗ trợ bất kỳ bố cục nào. Sự lựa chọn giữa chúng phụ thuộc vào cấu trúc dữ liệu trực quan và mức độ tùy chỉnh yêu cầu.
| Tham số | UITableView | UICollectionView |
|---|---|---|
| Bố cục | Chỉ danh sách dọc | Danh sách, lưới, thác, băng chuyền, tùy chỉnh |
| Độ phức tạp | Thấp (DataSource đơn giản) | Trung bình (Layout + DataSource + Delegate) |
| Ô mặc định | 4 kiểu (basic, subtitle, value1, value2) | Chỉ trống (tùy chỉnh qua contentView) |
| Chế độ chỉnh sửa | Tích hợp (delete, move, insert) | Yêu cầu triển khai thủ công |
| Header/Footer | Tích hợp (viewForHeaderInSection) | Qua supplementary views |
| Hiệu suất | Cao cho danh sách | Cao cho bất kỳ bố cục nào |
| iOS 6+ | Có sẵn từ iOS 2 | Có sẵn từ iOS 6 |
Quy tắc thực tế: sử dụng UITableView nếu màn hình là danh sách hàng đơn giản (cài đặt, danh bạ, menu). Sử dụng UICollectionView nếu màn hình yêu cầu lưới, cuộn ngang, băng chuyền, kích thước ô khác nhau hoặc nhóm phức tạp. Từ iOS 14, UITableView cũng đã hỗ trợ CompositionalLayout qua UICollectionLayoutListConfiguration, cho phép kết hợp lợi ích của cả hai thành phần.
Câu hỏi thường gặp
Đặt scrollDirection = .horizontal trong UICollectionViewFlowLayout. Đối với CompositionalLayout, sử dụng NSCollectionLayoutSize với fractionalWidth cho chiều rộng nhóm thích ứng. Cuộn ngang phù hợp cho băng chuyền, thư viện ảnh và danh mục. Để phân trang, đặt collectionView.isPagingEnabled = true hoặc sử dụng UICollectionViewFlowLayout với itemSize = bounds.width.
Kiểm tra: (1) ô đã được đăng ký qua register(_:forCellWithReuseIdentifier:), (2) DataSource trả về số lượng mục > 0, (3) ô được dequeue với reuseIdentifier đúng, (4) itemSize không bằng 0 (cho FlowLayout). Một lỗi điển hình là itemSize = CGSize.zero, làm cho ô có chiều cao bằng không. Đặt itemSize rõ ràng hoặc triển khai ủy quyền collectionView(_:layout:sizeForItemAt:).
Đối với FlowLayout: layout.minimumInteritemSpacing (giữa các mục trong một hàng) và layout.minimumLineSpacing (giữa các hàng). Đối với CompositionalLayout: NSCollectionLayoutGroup.interItemSpacing và contentInsets trên nhóm/phần. sectionInset đặt lề bên ngoài của phần. Sử dụng sectionInsetReference = .fromContentInset để xử lý Safe Area chính xác.
Sử dụng performBatchUpdates cho các hoạt ảnh nhóm: collectionView.performBatchUpdates { insertItems, deleteItems, reloadItems }. Để cập nhật an toàn và có hoạt ảnh, sử dụng UICollectionViewDiffableDataSource: gọi dataSource.apply(snapshot, animatingDifferences: true) — tất cả thay đổi sẽ được tính toán và áp dụng tự động mà không cần insert/delete thủ công.
Trong SwiftUI, tương đương với UICollectionView là LazyVGrid và LazyHGrid (iOS 14+). SwiftUI Grid dễ viết hơn (cú pháp khai báo) nhưng kém hơn UICollectionView về hiệu suất cho 500+ phần tử và tùy chỉnh bố cục. Đối với bộ sưu tập phức tạp, hãy sử dụng UICollectionView qua UIViewRepresentable. Trong các dự án IT Sectr, chúng tôi chọn UICollectionView cho danh mục và thư viện ảnh, và SwiftUI Grid cho các màn hình đơn giản.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm