Hướng dẫn custom hiển thị card với UICollectionViewLayout
UICollectionViewLayout là lớp trừu tượng cơ bản. Bạn có thể kế thừa từ nó để tạo ra và bố cục layout cho UICollectionView. Công việc bố cục tạo ra chủ yếu cho vị trí của cell, supplementary views và decoration views trong UICollectionView. Khi đó UICollectionView sẽ sử dụng những thông tin ...
UICollectionViewLayout là lớp trừu tượng cơ bản. Bạn có thể kế thừa từ nó để tạo ra và bố cục layout cho UICollectionView. Công việc bố cục tạo ra chủ yếu cho vị trí của cell, supplementary views và decoration views trong UICollectionView. Khi đó UICollectionView sẽ sử dụng những thông tin trong đó để thể hiện nội dụng lên màn hình cho chung ta thấy.
Những func cần thiết để override
- collectionViewContentSize : Dùng để trả về content size cho UICollectionView sau khi đã bố cục layout.
- layoutAttributesForElements(in:) : Trả về các element sẽ hiện thị trong khung hình.
- layoutAttributesForItem(at:) : Trả về các item (cell) ứng với indexPath.
- layoutAttributesForSupplementaryView(ofKind:at:): Trả về các item SupplementaryView (Header hoặc Footer) ứng với indexPath.
- prepare(): Sử dụng func này để tính toán và thiết lập các item cho collection view. Apple khuyến cáo nếu muốn cache lại thì nên sử dụng func này
- shouldInvalidateLayout(forBoundsChange:): Check sự thay đổi layout cho collection view.
- invalidateLayout(): Huỷ bỏ bố cục hiện tại và thiết lập lại cho bố cục mới.
1. Định nghĩa delegate cho collection view layout
Bước đầu, cần định nghĩa delegate cho collection view layout để có thể nhận được các thông tin cho việc custom collection view layout như size items , size header, section insets
protocol ATMCollectionViewLayoutDelegate: class { func collectionViewLayout(collecitionViewLayout: CardCollectionViewLayout, sizeForItem indexPath: IndexPath) -> CGSize func collectionViewLayout(colletionViewLayout: CardCollectionViewLayout, insetForSection section: Int) -> UIEdgeInsets func collectionViewLayout(collectionViewLayout: CardCollectionViewLayout, sizeForHeaderSection section: Int) -> CGSize }
Sử dụng extension để thiết lập các giá trị ban đầu cho delegate. Cách này có thể giúp khi controller conform từ delegate không cần phải implement hết toàn bộ function trong delegate. Có thể sử dụng cách này thay có optional func trong protocol của Objective C
extension ATMCollectionViewLayoutDelegate { func collectionViewLayout(collecitionViewLayout: ATMCollectionViewLayout, sizeForItem indexPath: IndexPath) -> CGSize { return CGSize.zero } func collectionViewLayout(colletionViewLayout: ATMCollectionViewLayout, insetForSection section: Int) -> UIEdgeInsets { return UIEdgeInsets.zero } func collectionViewLayout(collectionViewLayout: ATMCollectionViewLayout, sizeForHeaderSection section: Int) -> CGSize { return CGSize.zero } }
2.Tạo subclass cho UICollectionViewLayout
Đầu tiên, cần tạo 1 subclass từ UICollectionViewLayout có tên là CardCollectionViewLayout. Trong này, sẽ dùng để override các function phía trên để custom lại layout cho collection giống như trong demo.
weak var delegate: ATMCollectionViewLayoutDelegate? //Khai báo chiều cao của thẻ var itemHeight: CGFloat = 40 //Xác định hướng scroll cho collection view var scrollDirection: UICollectionViewScrollDirection = .vertical var contentWidth: CGFloat = 0 var contentHeight: CGFloat = 0 // Định nghĩa 1 mảng itemAttributes dùng để lưu lại các item là cell private var itemAttributesCache: Array<UICollectionViewLayoutAttributes> = [] // Định nghĩa 1 mảng headerAttributes dùng để lưu lại các item là header private var headerAttributesCache: Array<UICollectionViewLayoutAttributes> = []
Chuẩn bị tính toán và bố cục layout cho collection view. Ta override lại function prepare() và tính toán layout ở trong đó
override func prepare() { super.prepare() // Kiểm tra đã khởi tạo layout chưa, nếu có rồi thì không tính toán lại layout guard itemAttributesCache.isEmpty, headerAttributesCache.isEmpty, let collectionView = collectionView else { return } // Tính toán phần dimension theo vertical hoặc horizontal let fixedDimension: CGFloat if scrollDirection == .vertical { fixedDimension = collectionView.frame.awidth - (collectionView.contentInset.left + collectionView.contentInset.right) contentWidth = fixedDimension } else { fixedDimension = collectionView.frame.height - (collectionView.contentInset.top + collectionView.contentInset.bottom) contentHeight = fixedDimension } //Sử dụng để lưu lại khoảng cách của các item và header var additionalSectionSpacing: CGFloat = 0 for section in 0..<collectionView.numberOfSections { //Lấy size của header view nếu có let sizeHeaderSection = (delegate ?? self).collectionViewLayout(collectionViewLayout: self, sizeForHeaderSection: section) //Số lượng item của section let itemCount = collectionView.numberOfItems(inSection: section) //Lấy content inset của section nếu có let sectionInset = (delegate ?? self).collectionViewLayout(colletionViewLayout: self, insetForSection: section) //Kiểm tra điều kiện để tính toán layout cho header section if sizeHeaderSection.awidth > 0 && sizeHeaderSection.height > 0 && itemCount > 0 { let frame: CGRect //Tính toán frame cho header section if scrollDirection == .vertical { frame = CGRect(x: 0, y: additionalSectionSpacing, awidth: sizeHeaderSection.awidth, height: sizeHeaderSection.height) } else { frame = CGRect(x: additionalSectionSpacing, y: 0, awidth: sizeHeaderSection.height, height: sizeHeaderSection.awidth) } //Khởi tạo layout attrubute của header và set frame let headerLayoutAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, with: IndexPath(item: 0, section: section)) headerLayoutAttribute.frame = frame headerLayoutAttribute.zIndex = section * 1000 // Đưa header vào mảng để sử dụng cho việc cache dữ liệu headerAttributesCache.append(headerLayoutAttribute) // Tính lại khoảng cách với additionalSectionSpacing += frame.height } //Tính lại khoảng cách với content inset của section if sizeHeaderSection.awidth > 0 && sizeHeaderSection.height > 0 { additionalSectionSpacing += scrollDirection == .vertical ? sectionInset.top : sectionInset.left } for item in 0..<itemCount { let indexPath = IndexPath(item: item, section: section) //Lấy item size của collection let itemSize = (delegate ?? self).collectionViewLayout(collecitionViewLayout: self, sizeForItem: indexPath) let frame: CGRect //Tính toán frame của item if scrollDirection == .vertical { let awidthItem = itemSize.awidth - (sectionInset.left + sectionInset.top) frame = CGRect(x: sectionInset.left, y: additionalSectionSpacing, awidth: awidthItem, height: itemSize.height) } else { let heightItem = itemSize.height - (sectionInset.top + sectionInset.bottom) frame = CGRect(x: additionalSectionSpacing, y:sectionInset.top , awidth: itemSize.awidth, height: heightItem) } //Khởi tạo layout attrubute của cell và set frame cho nó let itemLayoutAttribute = UICollectionViewLayoutAttributes(forCellWith: indexPath) itemLayoutAttribute.frame = frame //Set zIndex để xác định thứ tự layout itemLayoutAttribute.zIndex = section * 1000 + item //Add layout vào mảng item cho việc cache itemAttributesCache.append(itemLayoutAttribute) //Tính toán lại khoảng cách if item == itemCount - 1 { additionalSectionSpacing += scrollDirection == .vertical ? frame.height + sectionInset.bottom : frame.awidth + sectionInset.right } else { additionalSectionSpacing += itemHeight } if scrollDirection == .vertical { contentHeight = additionalSectionSpacing } else { contentWidth = additionalSectionSpacing } } } }
Xong phần chuẩn bị layout thì chuyển tiếp qua công việc kiểm tra và đưa layout ra ngoài. Lúc này ta sẽ cần override 3 function chính là :
- layoutAttributesForElements(in:) : Trả về các element sẽ hiện thị trong khung hình.
- layoutAttributesForItem(at:) : Trả về các item (cell) ứng với indexPath.
- layoutAttributesForSupplementaryView(ofKind:at:): Trả về các item SupplementaryView (Header hoặc Footer) ứng với indexPath. Đây là 3 function sẽ nhận về các layout và hiện nó ra ngoài collection view
//Trả về các layout atrribute sẽ được hiện thị ở trong khung //Trả về các layout atrribute sẽ được hiện thị ở trong khung override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let headerInRect = headerAttributesCache.filter { (header) -> Bool in //Kiểm tra frame của header có giao với khung cần hiển thị header.frame.intersects(rect) } let itemInRect = itemAttributesCache.filter { (item) -> Bool in //Kiểm tra frame của item có giao với khung cần hiển thị return item.frame.intersects(rect) } return headerInRect + itemInRect } //Trả về item layout atrribute với vị trí là indexPath override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return itemAttributesCache.first { return $0.indexPath == indexPath } } //Trả về header hoặc footer layout atrribute với vị trí là indexPath override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { if elementKind == UICollectionElementKindSectionHeader { return headerAttributesCache.first{ $0.indexPath == indexPath } } return nil }
Kiểm tra khung hiển thị có bị thay đổi và yêu cầu update lại thông tin. Nó sẽ được gọi khi có sự thay đổi về frame của collection view hoặc do thây đổi hướng của thiết bị
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { if scrollDirection == .vertical, let oldWidth = collectionView?.bounds.awidth { return oldWidth != newBounds.awidth } if scrollDirection == .horizontal, let oldHeight = collectionView?.bounds.height { return oldHeight != newBounds.height } return false }
Cuối cùng là thiết lập lại dữ liệu nếu có sự thay đổi về bố cục hiển thị ở function phía trên.Lúc này funtion invalidateLayout() sẽ được gọi
override func invalidateLayout() { super.invalidateLayout() itemAttributesCache = [] headerAttributesCache = [] contentWidth = 0 contentHeight = 0 }
Mình đã hướng dẫn cơ bản cho các bạn việc custom collection view sử dụng UICollectionViewLayout.Các bạn có thể tại class custom về tại đây. Cảm ơn các bạn đã đọc bài viết của mình !