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()
        }
    }
}

效果如下:


iOS CollectionView 进阶_第1张图片

这些基础布局API还是有几个要说道的地方的

  1. dequeueReusableCell(withReuseIdentifier identifier: String, for indexPath: IndexPath)这个方法一定会返回一个cell,前提是identifier要注册过,不然抛异常
  2. forSupplementaryViewOfKind的参数虽然是一个String,但需要传UICollectionView.elementKindSectionHeaderUICollectionView.elementKindSectionFooter
  3. flowLayout可以直接设置headerReferenceSize/footerReferenceSize,这是default size,但是如果实现了代理collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int),会使用代理返回的值。重点是,如果是vertical滚动,只会用高度,宽度和collectionView一致,horizental滚动反过来
  4. UICollectionViewCell默认没有title等元素,完全一个白板,需要自己加UI组件(和UITableViewCell不同,后者默认有title等UI元素

自定义布局

我们都知道要使用CollectionView离不开两个类:UICollectionViewUICollectionViewLayout,其中后者掌握了CollectionView的布局。实际上,常用的有关布局的代理方法,比如sizeForItem的调用时机也是在UICollectionViewFlowLayoutprepare,注意,UICollectionViewFlowLayout自己实现的prepare中调用了sizeForItem代理,但是UICollectionViewLayout是不会自己调用prepare的,默认的是实现是空,举个例子吧:

重写UICollectionViewFlowLayoutprepare方法,并使用这个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

可以看到UICollectionViewFlowLayoutprepare方法调用了numberOfSections & numberOfItemsInSection & sizeForItemAt & referenceSizeForHeaderInSection 等涉及到布局的代理方法

但是UICollectionViewLayoutprepare方法的默认实现是空函数

瀑布流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.. 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
    }
}

同样的,有以下几点需要注意的:

  1. 观察prepare方法加的log输出,并没有调用sizeForItem代理方法
  2. contentHeight直到prepare结束才确定的
  3. 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 CollectionView 进阶_第2张图片

结交人脉

最后推荐个我的iOS交流群:789143298
'有一个共同的圈子很重要,结识人脉!里面都是iOS开发,全栈发展,欢迎入驻,共同进步!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

  • ——点击加入:iOS开发交流群
    以下资料在群文件可自行下载

    驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!**

你可能感兴趣的:(iOS CollectionView 进阶)