gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-05 03:28:21 26 4
gpt4 key购买 nike

我正在为 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/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com