swift专栏Swift

iOS CollectionView 进阶

2020-12-01  本文已影响0人  一意孤行的程序猿

前言

这篇文章讲一下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还是有几个要说道的地方的

  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..<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
    }
}

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

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

上一篇下一篇

猜你喜欢

热点阅读