Modern collection views
Bài đăng này đã không được cập nhật trong 5 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ề UICollectionViewvà cáctypeliên quan đến nó. Không nhữngAPImới được giới thiệu mà một số cácconcept, khái niệm đã từng được sử dụng đểbuildcollectionviewđã được thay đổi và cập nhập theo các mô hìnhprogrammingmớ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áccollectionnà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 viewtrê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 racrashapp khi mà cácupdatekế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 viewvà tự động thay đổiupdatecác thay đổi cần thiết cho việc hiển thịdata.
- 
Chúng ta lấy ví dụ về ProductListViewControllerhiển thị cácproduct. Đểviewcontrollersử dụngDiffableDataSourcethì đầu tiên chúng ta phải khởi tạo mộtcellProviderclosuređể chuyển cácindexPathchoUICollectionViewCellnhư 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 typenhư cách trên vừa đảm bảotype safecho cácmodel datacũng như cho phép chúng ta có thểcustomcác typeHashableđịnh nghĩa cho cácSectionthay 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 assigndatachocollection viewnhư 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đã đượcupdatechúng ta cần thêmdescribecác state củacurrent viewchodataSourceđể cáccellcó thể tự động theo dõi vàupdatekhi cần.
- 
Chúng ta sẽ sử dụng khái niệm snapshopcho cácsectionđã được định nghĩa và update cho từngsectiontừdata model. Cuối cùng chúng ta sử dụngsnapshotchodataSourcebằng cách cập nhậtcollection viewsau 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 Producttrực tiếp chodataSourcebằng cáchconfirmHashable. Cách làm trên có vấn đề nếu chúng ta códata modelkhông thểconfirmcácprotocoltrên nên chúng ta có thể chuyển một số định dạng chodataSourcevà sau đó sẽ tiến hành cho cácmodelhoàn chỉnh trongcellProviderclosure.
2: Cell registrations:
- 
Cell registrationslà mộtconceptmới trongiOS14cho pháp chúng ta có thể định danh việc sử dụng subclassUICollectionViewCellcũng như hỗ trợ cách chúng ta tùy chỉnhcollectionview cellvớ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ạicellcho công việcreuse identifiervà các cell sẽ không cầntype casting.
- 
Chúng ta sẽ sử dụng APImới đểimplementviệcregistrationvàconfigurationchocollectionview cellvớiProductListViewControllernhư 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 makeDataSourcevà thay đổicellProvidernhư 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 codetrước đó vớicellProviderclosure đả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 extensiontrê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 iOS13chúng ta có 2 cách lựa chọn để tùy chỉnh layout choUICollectionView. Cách đầu tiên là sử dụngUICollectionViewFlowLayoutvà 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 layoutbao gồm: items, groups, sections. Item cho việclayoutcá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 layoutcho các product list view với cácfeaturedvàonSalesection đang sử sựng 2column gridtrong 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 layoutlà chúng ta có thể sử dụng nhiều layout cho trong mộtviewcontrollercũng như có thểdescribelayoutmong 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:
- Ở iOS14chúng ta hoàn toàn có thể buildtable viewbằ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ĩalisttrướ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 layoutcode nữa mà chỉ cần sử dụng lạiinsetGroupđể có cáclayoutmong 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``UICollectionViewListCellnhư một cáchcopytheoUITableViewCellđể có thể render cáctext,imagecũng như cácaccesoriesnhưindicator.makeCellRegistrationmethod 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
 
  
 