iOS CollectionView 进阶
前言
这篇文章讲一下CollectionView的高级用法,比如自定义布局
自定义布局
先写个入门的布局代码:
import UIKit
class ViewController: UIViewController,UICollectionViewDataSource,UICollectionViewDelegate {
var flowLayout : UICollectionViewFlowLayout?
var collectionView : UICollectionView?
var items : [[String]]?
override func viewDidLoad() {
super.viewDidLoad()
buildData()
flowLayout = UICollectionViewFlowLayout.init()
flowLayout?.minimumLineSpacing = 20
flowLayout?.minimumInteritemSpacing = 10
flowLayout?.itemSize = CGSize(width: 65, height: 35)
flowLayout?.scrollDirection = .vertical
flowLayout?.headerReferenceSize = CGSize(width: 150, height: 50)
flowLayout?.footerReferenceSize = CGSize(width: 130, height: 50)
flowLayout?.sectionInset = .init(top: 10, left: 10, bottom: 10, right: 10)
let screenBounds = UIScreen.main.bounds;
let collectionFrame = CGRect(x: 0, y: 50, width: screenBounds.width, height: screenBounds.height-100)
collectionView = UICollectionView(frame: collectionFrame, collectionViewLayout: flowLayout!)
collectionView?.backgroundColor = .gray
collectionView?.alwaysBounceVertical = true
view.addSubview(collectionView!)
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
collectionView?.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header")
collectionView?.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "footer")
collectionView?.dataSource = self
collectionView?.delegate = self
}
private func buildData() {
items = [["1:1","1:2","1:3","1:4","1:5","1:6","1:7","1:8","1:9","1:10","1:11","1:12","1:13","1:14","1:15","1:16"],["2:1","2:2","2:3","2:4","2:5","2:6","2:7","2:8","2:9","2:10","2:11","2:12","2:13","2:14","2:15","2:16","2:17","2:18","2:19","2:20"]]
}
//MARK:- data source
func numberOfSections(in collectionView: UICollectionView) -> Int {
return items!.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items![section].count
}
//MARK:- delegate
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = .green
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header", for: indexPath)
header.backgroundColor = .blue
return header
} else if kind == UICollectionView.elementKindSectionFooter {
let footer = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "footer", for: indexPath)
footer.backgroundColor = .orange
return footer
} else {
return UICollectionReusableView()
}
}
}
效果如下:
这些基础布局API还是有几个要说道的地方的
-
dequeueReusableCell(withReuseIdentifier identifier: String, for indexPath: IndexPath)
这个方法一定会返回一个cell,前提是identifier要注册过,不然抛异常 -
forSupplementaryViewOfKind
的参数虽然是一个String
,但需要传UICollectionView.elementKindSectionHeader
或UICollectionView.elementKindSectionFooter
-
flowLayout
可以直接设置headerReferenceSize/footerReferenceSize
,这是default size,但是如果实现了代理collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int)
,会使用代理返回的值。重点是,如果是vertical滚动,只会用高度,宽度和collectionView一致,horizental滚动反过来 -
UICollectionViewCell
默认没有title
等元素,完全一个白板,需要自己加UI组件(和UITableViewCell
不同,后者默认有title等UI元素
自定义布局
我们都知道要使用CollectionView
离不开两个类:UICollectionView
和UICollectionViewLayout
,其中后者掌握了CollectionView
的布局。实际上,常用的有关布局的代理方法,比如sizeForItem
的调用时机也是在UICollectionViewFlowLayout
的prepare
中,注意,UICollectionViewFlowLayout
自己实现的prepare
中调用了sizeForItem
代理,但是UICollectionViewLayout
是不会自己调用prepare
的,默认的是实现是空,举个例子吧:
重写UICollectionViewFlowLayout
的prepare
方法,并使用这个layout:
class CustomFlowLayout: UICollectionViewFlowLayout {
override func prepare() {
print("prepare1")
super.prepare()
print("prepare2")
itemSize = CGSize(width: 120, height: 50)
}
}
打印日志:
prepare1 # 调用super.prepare前
numberOfSections
numberOfItemsInSection
numberOfItemsInSection
sizeForItemAt
sizeForItemAt
referenceSizeForHeaderInSection
sizeForItemAt
sizeForItemAt
referenceSizeForHeaderInSection
prepare2 # 调用super.prepare后
cellForItemAt
cellForItemAt
cellForItemAt
cellForItemAt
viewForSupplementaryElementOfKind
viewForSupplementaryElementOfKind
viewForSupplementaryElementOfKind
viewForSupplementaryElementOfKind
可以看到UICollectionViewFlowLayout
的prepare
方法调用了numberOfSections
& numberOfItemsInSection
& sizeForItemAt
& referenceSizeForHeaderInSection
等涉及到布局的代理方法
但是UICollectionViewLayout
的prepare
方法的默认实现是空函数
瀑布流Pinterest-Like布局
下面着手写一个瀑布流的demo:
class CustomCollectionLayout: UICollectionViewLayout {
private var attrCache : [UICollectionViewLayoutAttributes] = []
private let columnNum = 2
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
private var contentWidth : CGFloat {
guard let collectionView = collectionView else {
return 0
}
let inset = collectionView.contentInset
return collectionView.bounds.width - (inset.left + inset.right)
}
private var contentHeight : CGFloat = 0
private let cellPadding : CGFloat = 5
override func prepare() {
print("prepare1")
super.prepare()
guard let collectionView = collectionView,
attrCache.count == 0 else {
return
}
var columns : [CGFloat] = Array.init(repeating: 0, count: columnNum)
for idx in 0..<collectionView.numberOfItems(inSection: 0) {
let offsetXIdx = idx % 2
let x = CGFloat(offsetXIdx) * (contentWidth / 2)
let y = columns[offsetXIdx]
let width = contentWidth/2
let height = randomHeight() + cellPadding * 2
var frame = CGRect(x: x, y: y, width: width, height: height)
frame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let indexPath = IndexPath(item: idx, section: 0)
let collectionAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
collectionAttributes.frame = frame
attrCache.append(collectionAttributes)
columns[offsetXIdx] += height
}
contentHeight = columns.max()!
print("prepare2")
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard attrCache.count > 0 else {
return nil
}
return attrCache[indexPath.item]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard attrCache.count > 0 else {
return nil
}
var res : [UICollectionViewLayoutAttributes] = []
for attr in attrCache {
if attr.frame.intersects(rect) {
res.append(attr)
}
}
return res
}
private func randomHeight() -> CGFloat {
let randomHeight = CGFloat.random(in: 50...150)
print("randowm:\(randomHeight)")
return randomHeight
}
}
同样的,有以下几点需要注意的:
- 观察
prepare
方法加的log输出,并没有调用sizeForItem
代理方法 -
contentHeight
直到prepare结束才确定的 - 加
cacheAttr
的原因:
Since prepare() is called whenever the collection view's layout becomes invalid, there are many situations in a typical implementation where you might need to recalculate attributes here. For example, the bounds of the UICollectionView might change when the orientation changes. They could also change if items are added or removed from the collection.
demo截图如下:
结交人脉
最后推荐个我的iOS交流群:789143298
'有一个共同的圈子很重要,结识人脉!里面都是iOS开发,全栈发展,欢迎入驻,共同进步!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)
-
——点击加入:iOS开发交流群
以下资料在群文件可自行下载
驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!**