Modern collection views
Bài đăng này đã không được cập nhật trong 4 năm
- Trong thời quan 2 năm từ iOS13-iOS14 chúng ta đã thấy một số thay đổi đến từ Apple về
UICollectionView
và cáctype
liên quan đến nó. Không nhữngAPI
mới được giới thiệu mà một số cácconcept
, khái niệm đã từng được sử dụng đểbuild
collectionview
đã được thay đổi và cập nhập theo các mô hìnhprogramming
mới. Ở bài viết này chúng ta sẽ cumgf tìm hiểu các mô hình mới để hiểu thêm cáccollection
này hoạt động ra sao.
1: Diffable data sources:
-
Một trong những vấn đề chúng ta thường gặp lúc làm việc với
collection view
trên hệ thống từ trướciOS13
đến từ các trường hợp trong thực tế khi tất cả các cập nhật cần phải được triển khai thủ công bởi cácdeveloper
( như cách sử dụngperformBatchUpdates
). Cách triển khai thủ công trên thường gây racrash
app khi mà cácupdate
kết thức không đông thời với cácdata model
đang được sử dụng. -
Sử dụng
UICollectionViewDiffableDataSource
đồng nghĩa với việc chúng ta sẽ choclass
đó tính toán sự thay đổi trạng thái củacollection view
và tự động thay đổiupdate
các thay đổi cần thiết cho việc hiển thịdata
. -
Chúng ta lấy ví dụ về
ProductListViewController
hiển thị cácproduct
. Đểviewcontroller
sử dụngDiffableDataSource
thì đầu tiên chúng ta phải khởi tạo mộtcellProvider
closure
để chuyển cácindexPath
choUICollectionViewCell
như sau:
private extension ProductListViewController {
func makeDataSource() -> UICollectionViewDiffableDataSource<Section, Product> {
UICollectionViewDiffableDataSource(
collectionView: collectionView,
cellProvider: { collectionView, indexPath, product in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: Self.cellReuseID,
for: indexPath
) as! ListCollectionViewCell
cell.textLabel.text = product.name
...
return cell
}
)
}
}
- Sử dụng
Swift strong type
như cách trên vừa đảm bảotype safe
cho cácmodel data
cũng như cho phép chúng ta có thểcustom
các typeHashable
định nghĩa cho cácSection
thay vì luôn sử dụngInt
:
private extension ProductListViewController {
enum Section: Int, CaseIterable {
case featured
case onSale
case all
}
}
- Điều cần làm bây giờ là chúng ta cần
assign
data
chocollection view
như cách chúng ta đã sử dụng trước đó:
class ProductListViewController: UIViewController {
private static let cellReuseID = "product-cell"
private lazy var collectionView = makeCollectionView()
private lazy var dataSource = makeDataSource()
...
override func viewDidLoad() {
super.viewDidLoad()
// Registering our cell class with the collection view
// and assigning our diffable data source to it:
collectionView.register(ListCollectionViewCell.self,
forCellWithReuseIdentifier: Self.cellReuseID
)
collectionView.dataSource = dataSource
...
}
...
}
-
Khi các
data model
đã đượcupdate
chúng ta cần thêmdescribe
các state củacurrent view
chodataSource
để cáccell
có thể tự động theo dõi vàupdate
khi cần. -
Chúng ta sẽ sử dụng khái niệm
snapshop
cho cácsection
đã được định nghĩa và update cho từngsection
từdata model
. Cuối cùng chúng ta sử dụngsnapshot
chodataSource
bằng cách cập nhậtcollection view
sau khi so sánh sự thay đổi trước đó:
private extension ProductListViewController {
func productListDidLoad(_ list: ProductList) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Product>()
snapshot.appendSections(Section.allCases)
snapshot.appendItems(list.featured, toSection: .featured)
snapshot.appendItems(list.onSale, toSection: .onSale)
snapshot.appendItems(list.all, toSection: .all)
dataSource.apply(snapshot)
}
}
- Nên lưu ý ở đây là chúng ta đang chuyển model
Product
trực tiếp chodataSource
bằng cáchconfirm
Hashable
. Cách làm trên có vấn đề nếu chúng ta códata model
không thểconfirm
cácprotocol
trên nên chúng ta có thể chuyển một số định dạng chodataSource
và sau đó sẽ tiến hành cho cácmodel
hoàn chỉnh trongcellProvider
closure.
2: Cell registrations:
-
Cell registrations
là mộtconcept
mới trongiOS14
cho pháp chúng ta có thể định danh việc sử dụng subclassUICollectionViewCell
cũng như hỗ trợ cách chúng ta tùy chỉnhcollectionview cell
với các object phức tạp. Chúng ta sẽ không cần nhớ tới việc phải khai báo chính các các loạicell
cho công việcreuse identifier
và các cell sẽ không cầntype casting
. -
Chúng ta sẽ sử dụng
API
mới đểimplement
việcregistration
vàconfiguration
chocollectionview cell
vớiProductListViewController
như sau:
private extension ProductListViewController {
typealias Cell = ListCollectionViewCell
typealias CellRegistration = UICollectionView.CellRegistration<Cell, Product>
func makeCellRegistration() -> CellRegistration {
CellRegistration { cell, indexPath, product in
cell.textLabel.text = product.name
...
}
}
}
- Chúng ta có thể xem lại method
makeDataSource
và thay đổicellProvider
như sau:
private extension ProductListViewController {
func makeDataSource() -> UICollectionViewDiffableDataSource<Section, Product> {
let cellRegistration = makeCellRegistration()
return UICollectionViewDiffableDataSource(
collectionView: collectionView,
cellProvider: { collectionView, indexPath, product in
collectionView.dequeueConfiguredReusableCell(
using: cellRegistration,
for: indexPath,
item: product
)
}
)
}
}
- Chúng ta vừa cải thiện đáng kể đoạn
code
trước đó vớicellProvider
closure đảm nhận trực tiếp việc cellregistration
. Chúng ta sẽ cần thêm mộtextension
ở đây:
extension UICollectionView.CellRegistration {
var cellProvider: (UICollectionView, IndexPath, Item) -> Cell {
return { collectionView, indexPath, product in
collectionView.dequeueConfiguredReusableCell(
using: self,
for: indexPath,
item: product
)
}
}
}
- Với
extension
trên chúng ta đã giảm số dòng code trên như sau:
private extension ProductListViewController {
func makeDataSource() -> UICollectionViewDiffableDataSource<Section, Product> {
UICollectionViewDiffableDataSource(
collectionView: collectionView,
cellProvider: makeCellRegistration().cellProvider
)
}
}
3: Compositional layouts:
-
Trước
iOS13
chúng ta có 2 cách lựa chọn để tùy chỉnh layout choUICollectionView
. Cách đầu tiên là sử dụngUICollectionViewFlowLayout
và chúng ta sẽ cùng thực hiện từ những bước đầu tiên cho lựa chọn này: -
Chúng ta cần định nghĩa rõ cho các
compositional layout
bao gồm: items, groups, sections. Item cho việclayout
cáccell
, group cho việc layout các cell với nhau và các sectionsex bao gồm các section chocollectionview
. -
Chúng ta muốn
layout
cho các product list view với cácfeatured
vàonSale
section đang sử sựng 2column grid
trong khi các section đang sử dụngfull-width
:
private extension ProductListViewController {
func makeGridLayoutSection() -> NSCollectionLayoutSection {
// Each item will take up half of the width of the group
// that contains it, as well as the entire available height:
let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.5),
heightDimension: .fractionalHeight(1)
))
// Each group will then take up the entire available
// width, and set its height to half of that width, to
// make each item square-shaped:
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalWidth(0.5)
),
subitem: item,
count: 2
)
return NSCollectionLayoutSection(group: group)
}
}
- Điểm mạnh của
compositional layout
là chúng ta có thể sử dụng nhiều layout cho trong mộtviewcontroller
cũng như có thểdescribe
layout
mong muốn của chúng ta sử dụngfractional values
:
private extension ProductListViewController {
func makeListLayoutSection() -> NSCollectionLayoutSection {
// Here, each item completely fills its parent group:
let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
))
// Each group then contains just a single item, and fills
// the entire available width, while defining a fixed
// height of 50 points:
let group = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(50)
),
subitems: [item]
)
return NSCollectionLayoutSection(group: group)
}
}
- Để tối ưu đoạn code trên chúng ta sẽ sử dụng
NSCollectionLayoutSection
để lấy section index theo dạngInt
:
private extension ProductListViewController {
func makeCollectionViewLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout {
[weak self] sectionIndex, _ in
switch Section(rawValue: sectionIndex) {
case .featured, .onSale:
return self?.makeGridLayoutSection()
case .all:
return self?.makeListLayoutSection()
case nil:
return nil
}
}
}
}
- Điều cuối cùng chúng ta cần làm là
inject
đoạn code trên mỗi khicollectionView
được khởi tao:
private extension ProductListViewController {
func makeCollectionView() -> UICollectionView {
UICollectionView(
frame: .zero,
collectionViewLayout: makeCollectionViewLayout()
)
}
}
4: List views and content configurations:
- Ở
iOS14
chúng ta hoàn toàn có thể buildtable view
bằng cách sử dụngUICollectionView
. Để render các section chúng ta đơn giản có thể sử dụng các định nghĩalist
trước đó thay vì tự tạo riêng:
private extension ProductListViewController {
func makeCollectionViewLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout {
[weak self] sectionIndex, environment in
switch Section(rawValue: sectionIndex) {
case .featured, .onSale:
return self?.makeGridLayoutSection()
case .all:
// Creating our table view-like list layout using
// a given appearence. Here we simply use 'plain':
return .list(
using: UICollectionLayoutListConfiguration(
appearance: .plain
),
layoutEnvironment: environment
)
case nil:
return nil
}
}
}
}
- Đoạn code trên đã khá tối ưu, chúng ta không cần phải viết các
custom layout
code nữa mà chỉ cần sử dụng lạiinsetGroup
để có cáclayout
mong muốn:
private extension ProductListViewController {
func makeCollectionView() -> UICollectionView {
let layout = UICollectionViewCompositionalLayout.list(
using: UICollectionLayoutListConfiguration(
appearance: .insetGrouped
)
)
return UICollectionView(
frame: .zero,
collectionViewLayout: layout
)
}
}
- Chúng ta cũng có thể tạo và sử dụng
type``UICollectionViewListCell
như một cáchcopy
theoUITableViewCell
để có thể render cáctext
,image
cũng như cácaccesories
nhưindicator
.makeCellRegistration
method có thể được tùy chình để chúng ta sử dụng như sau:
private extension ProductListViewController {
typealias Cell = UICollectionViewListCell
typealias CellRegistration = UICollectionView.CellRegistration<Cell, Product>
func makeCellRegistration() -> CellRegistration {
CellRegistration { cell, indexPath, product in
var config = cell.defaultContentConfiguration()
config.text = product.name
...
cell.contentConfiguration = config
cell.accessories = [.disclosureIndicator()]
}
}
}
All rights reserved