swift - 水平 UICollectionView UIKIT 需要标题在顶部

标签 swift uicollectionview uikit uicollectionviewflowlayout uicollectionreusableview

我正在为 UICollectionView 实现自定义 header

我需要在顶部为 Horizo​​ntal CollectionView 添加标题。

enter image description here

最佳答案

简而言之,创建自定义 UICollectionViewLayout是获得所需结果的一种方法。这是一个相当复杂的主题,因此解释很长。

在添加一些代码之前,您需要了解 UICollectionViewLayout 中的不同组件。例如单元格、补充 View 和装饰 View 。检查this out to know more

在您的示例中, header 上方称为 Supplementary views类型为 UICollectionReusableView并且这些单元格是您的 UICollectionViewCells 类型的单元格.

将布局分配给 Collection View 时, Collection View 对其布局进行一系列调用以了解如何布置 View 。

这是我发现的一个例子:

UICollectionView Custom Layout

来源:https://www.raywenderlich.com/4829472-uicollectionview-custom-layout-tutorial-pinterest

在您的自定义布局中,您必须 override这些来创建你想要的布局:

  • 准备()
  • collectionViewContentSize
  • layoutAttributesForElements(in:)
  • layoutAttributesForItem(at:)
  • layoutAttributesForSupplementaryView(ofKind:, at:)
  • 无效布局()
  • shouldInvalidateLayout(forBoundsChange)

设置布局有点复杂,主要是因为涉及数学,但归根结底,它只是为不同的单元格和补充 View 指定框架(x、y、宽度、高度)不同的视口(viewport)。

自定义布局示例

首先,我创建了一个非常基本的可重用 View ,用作每个部分中单元格上方的标题,其中只有一个标签

class HeaderView: UICollectionReusableView
{
    let title = UILabel()
    
    static let identifier = "CVHeader"
    
    override init(frame: CGRect)
    {
        super.init(frame: frame)
        layoutInterface()
    }

    required init(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)!
        layoutInterface()
    }

    func layoutInterface()
    {
        backgroundColor = .clear
        title.translatesAutoresizingMaskIntoConstraints = false
        title.backgroundColor = .clear
        title.textAlignment = .left
        title.textColor = .black
        addSubview(title)
        
        addConstraints([
        
            title.leadingAnchor.constraint(equalTo: leadingAnchor),
            title.topAnchor.constraint(equalTo: topAnchor),
            title.trailingAnchor.constraint(equalTo: trailingAnchor),
            title.bottomAnchor.constraint(equalTo: bottomAnchor)
            
        ])
    }
}

接下来我以正常方式设置我的 Collection View ,唯一的区别是我提供了一个自定义布局类

// The collection view
private var collectionView: UICollectionView!

// A random data source
let colors: [UIColor] = [.systemBlue, .orange, .purple]

private func configureCollectionView()
{
    collectionView = UICollectionView(frame: CGRect.zero,
                                      collectionViewLayout: createLayout())
    
    collectionView.backgroundColor = .lightGray
    
    collectionView.register(UICollectionViewCell.self,
                            forCellWithReuseIdentifier: "cell")
    
    // This is for the section titles 
    collectionView.register(HeaderView.self,
                            forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                            withReuseIdentifier: HeaderView.identifier)
    
    collectionView.dataSource = self
    collectionView.delegate = self
    
    view.addSubview(collectionView)
    
    // Auto layout config to pin collection view to the edges of the view
    collectionView.translatesAutoresizingMaskIntoConstraints = false
    
    collectionView.leadingAnchor
        .constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor,
                    constant: 0).isActive = true
    
    collectionView.topAnchor
        .constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
                    constant: 0).isActive = true
    collectionView.trailingAnchor
        
        .constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor,
                    constant: 0).isActive = true
    
    collectionView.heightAnchor
        .constraint(equalToConstant: 300).isActive = true
}

private func createLayout() -> HorizontalLayout
{
    // Not flow layout, but our custom layout
    let customLayout = HorizontalLayout()
    customLayout.itemSpacing = 10
    customLayout.sectionSpacing = 20
    customLayout.itemSize = CGSize(width: 50, height: 50)
    
    return customLayout
}

数据源和委托(delegate)也没有太大的不同,但我添加它是为了完整性

extension ViewController: UICollectionViewDataSource
{
    func numberOfSections(in collectionView: UICollectionView) -> Int
    {
        // random number
        return 3
    }
    
    func collectionView(_ collectionView: UICollectionView,
                        numberOfItemsInSection section: Int) -> Int
    {
        // random number
        return 8 + section
    }
    
    func collectionView(_ collectionView: UICollectionView,
                        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
    {
        let cell = collectionView
            .dequeueReusableCell(withReuseIdentifier:"cell",
                                 for: indexPath)
        
        cell.backgroundColor = colors[indexPath.section]
        
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        referenceSizeForHeaderInSection section: Int) -> CGSize
    {
        return CGSize(width: 200, height: 50)
    }
}

extension ViewController: UICollectionViewDelegate
{
    func collectionView(_ collectionView: UICollectionView,
                        viewForSupplementaryElementOfKind kind: String,
                        at indexPath: IndexPath) -> UICollectionReusableView
    {
        let header
            = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader,
                                                              withReuseIdentifier: HeaderView.identifier,
                                                              for: indexPath) as! HeaderView
        
        header.title.text = "Section \(indexPath.section)"
        
        return header
    }
}

最后,最复杂的部分,创建自定义布局类。从查看和理解我们跟踪的实例变量开始,这将根据您要创建的布局发生变化

class HorizontalLayout: UICollectionViewLayout
{
    // Cache layout attributes for the cells
    private var cellLayoutCache: [IndexPath: UICollectionViewLayoutAttributes] = [:]
    
    // Cache layout attributes for the header
    private var headerLayoutCache: [Int: UICollectionViewLayoutAttributes] = [:]
    
    // Set a y offset so the items render a bit lower which
    // leaves room for the title at the top
    private var sectionTitleHeight = CGFloat(60)
    
    // The content height of the layout is static since we're configuring horizontal
    // layout. However, the content width needs to be calculated and returned later
    private var contentWidth = CGFloat.zero
    
    private var contentHeight: CGFloat
    {
        guard let collectionView = collectionView else { return 0 }
        
        let insets = collectionView.contentInset
        
        return collectionView.bounds.height - (insets.left + insets.right)
    }
    
    // Based on the height of the collection view, the interItem spacing and
    // the item height, we can set
    private var maxItemsInRow = 0
    
    // Set the spacing between items & sections
    var itemSpacing: CGFloat = .zero
    var sectionSpacing: CGFloat = .zero
    
    var itemSize: CGSize = .zero
    
    override init()
    {
        super.init()
    }
    
    required init?(coder: NSCoder)
    {
        super.init(coder: coder)
    }

然后我们需要一个函数来帮助我们根据单元格高度、间距和基于 Collection View 高度的可用空间来计算一列中可以容纳多少个项目。如果当前列中没有更多空间,这将帮助我们将单元格移动到下一列:

private func updateMaxItemsInColumn()
{
    guard let collectionView = collectionView else { return }
    
    let contentHeight = collectionView.bounds.height
    
    let totalInsets
        = collectionView.contentInset.top + collectionView.contentInset.bottom
    
    // The height we have left to render the cells in
    let availableHeight = contentHeight - sectionTitleHeight - totalInsets
    
    // Set the temp number of items in a column as we have not
    // accounted for item spacing
    var tempItemsInColumn = Int(availableHeight / itemSize.height)
    
    // Figure out max items allowed in a row based on spacing settings
    while tempItemsInColumn != 0
    {
        // There is 1 gap between 2 items, 2 gaps between 3 items etc
        let totalSpacing = CGFloat(tempItemsInColumn - 1) * itemSpacing
        
        let finalHeight
            = (CGFloat(tempItemsInColumn) * itemSize.height) + totalSpacing
        
        if availableHeight < finalHeight
        {
            tempItemsInColumn -= 1
            continue
        }
        
        break
    }
    
    maxItemsInRow = tempItemsInColumn
}

接下来,我们需要一个函数来帮助我们找到一个部分的宽度,因为我们需要知道标题 View 应该有多长

private func widthOfSection(_ section: Int) -> CGFloat
{
    guard let collectionView = collectionView else { return .zero }
    
    let itemsInSection = collectionView.numberOfItems(inSection: section)
    
    let columnsInSection = itemsInSection / maxItemsInRow
    
    // There is 1 gap between 2 items, 2 gaps between 3 items etc
    let totalSpacing = CGFloat(itemsInSection - 1) * itemSpacing
    
    let totalWidth = (CGFloat(columnsInSection) * itemSize.width) + totalSpacing
    
    return totalWidth
}

并且如上所述,我们需要覆盖一些函数和属性。让我们从prepare()开始

// This function gets called before the collection view starts the layout process
// load layout into the cache so it doesn't have to be recalculated each time
override func prepare()
{
    guard let collectionView = collectionView else { return }
    
    // Only calculate if the cache is empty
    guard cellLayoutCache.isEmpty else { return }
    
    updateMaxItemsInColumn()
    
    let sections = 0 ... collectionView.numberOfSections - 1
    
    // Track the x position of the items being drawn
    var itemX = CGFloat.zero
        
    // Loop through all the sections
    for section in sections
    {
        var itemY = sectionTitleHeight
        var row = 0
        
        let headerFrame = CGRect(x: itemX,
                                 y: 0,
                                 width: widthOfSection(section),
                                 height: sectionTitleHeight)
        
        let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                                                          with: IndexPath(item: 0, section: section))
        attributes.frame = headerFrame
        headerLayoutCache[section] = attributes
        
        let itemsInSection = collectionView.numberOfItems(inSection: section)
        
        // Generate valid index paths for all items in the section
        let indexPaths = [Int](0 ... itemsInSection - 1).map
        {
            IndexPath(item: $0, section: section)
        }
        
        // Loop through all index paths and cache all the layout attributes
        // so it can be reused later
        for indexPath in indexPaths
        {
            let itemFrame = CGRect(x: itemX,
                               y: itemY,
                               width: itemSize.width,
                               height: itemSize.height)
            
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = itemFrame
            cellLayoutCache[indexPath] = attributes
            
            contentWidth = max(contentWidth, itemFrame.maxX)
            
            // last item in the section, update the x position
            // to start the next section in a new column and also
            // update the content width to add the section spacing
            if indexPath.item == indexPaths.count - 1
            {
                itemX += itemSize.width + sectionSpacing
                contentWidth = max(contentWidth, itemFrame.maxX + sectionSpacing)
                continue
            }
            
            if row < maxItemsInRow - 1
            {
                row += 1
                itemY += itemSize.height + itemSpacing
            }
            else
            {
                row = 0
                itemY = sectionTitleHeight
                itemX += itemSize.width + itemSpacing
            }
        }
    }
}

内容大小属性

// We need to set the content size. Since it is a horizontal
// collection view, the height will be fixed. The width should be
// the max X value of the last item in the collection view
override var collectionViewContentSize: CGSize
{
    return CGSize(width: contentWidth, height: contentHeight)
}

三个布局属性函数

// This defines what gets shown in the rect (viewport) the user
// is currently viewing
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
    // Get the attributes that fall in the current view port
    let itemAttributes = cellLayoutCache.values.filter { rect.intersects($0.frame) }
    let headerAttributes = headerLayoutCache.values.filter { rect.intersects($0.frame) }
    
    return itemAttributes + headerAttributes
}

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
{
    return cellLayoutCache[indexPath]
}

override func layoutAttributesForSupplementaryView(ofKind elementKind: String,
                                                   at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
{
    return headerLayoutCache[indexPath.section]
}

最后是无效布局

// In invalidateLayout(), the layout of the elements will be changing
// inside the collection view. Here the attribute cache can be reset
override func invalidateLayout()
{
    // Reset the attribute cache
    cellLayoutCache = [:]
    headerLayoutCache = [:]
    
    super.invalidateLayout()
}

// Invalidating the layout means the layout needs to be recalculated from scratch
// which might need to happen when the orientation changes so we only want to
// do this when it is necessary since it is expensive
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool
{
    guard let collectionView = collectionView else { return false }

    return newBounds.height != collectionView.bounds.height
}

所有这些都准备就绪后,您应该会得到以下结果:

Custom UICollectionView layout swift iOS UICollectionViewLayout flow layout customization

如果出于某种原因您无法将代码添加到正确的部分,请查看带有 complete source code here 的同一示例

虽然我无法涵盖所有​​内容,但您可以在完成此答案后查看以下 3 个很棒的教程以更深入地了解:

  1. Ray Weldenrich: UICollectionView Custom Layout Tutorial
  2. Building a Custom UICollectionViewLayout from Scratch
  3. Custom Collection View Layouts

关于swift - 水平 UICollectionView UIKIT 需要标题在顶部,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71274640/

相关文章:

swift - Xcode Swift 3 计时器错误

ios - 如何在使用自定义单元格的 UICollectionView 中正确设置异步图像下载?

ios - 在 iOS 上,为什么关闭 View Controller 后紧跟着当前 View Controller 不起作用?

ios - 使用 UIKit/Coregraphics 渲染为 PDF 时图像模糊

ios - 修改 UIGraphicsContext 中的原始 UIImage

ios - 如何在触摸单元格内的 slider 时取消 collectionView 手势?

ios - 在多个 Controller 上执行 segue

ios - 无法从 UICollectionViewCell 中删除图像

ios - UICollectionView 问题中的项目数

ios - 在 Swift 中使用嵌套的初始值设定项是不好的做法吗?