UICollectionView 不呈现所有行
UICollectionView not rendering all rows
我正在尝试创建一个简单的应用程序,其中可以为 UICollectionView 输入多列和多行。集合视图然后计算适合它的可能正方形的大小并绘制它们。
我想允许最大宽度为 32,高度为 64。滚动被禁用,因为整个网格应该一次显示。
例如,4x8 看起来像这样
8x4 看起来像这样
正如大家所见,它工作正常。问题伴随着更多的列 and/or 行。高达 30x8 一切都很好,但从 31 开始,8 行中只有 6 行被绘制。
所以我不明白为什么。以下是我用来计算所有内容的代码:
节数和行数:
func numberOfSections(in collectionView: UICollectionView) -> Int
{
let num = Int(heightInput.text!)
if(num != nil)
{
if(num! > 64)
{
return 64
}
return num!
}
return 8
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
let num = Int(widthInput.text!)
if(num != nil)
{
if(num! > 32)
{
return 32
}
return num!
}
return 4
}
indexPath 中项目的单元格
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let size = calculateCellSize()
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
var origin = cell.frame.origin
origin.x = 1+CGFloat(indexPath.row) + size.width*CGFloat(indexPath.row)
origin.y = 1+CGFloat(indexPath.section) + size.height*CGFloat(indexPath.section)
cell.frame = CGRect(origin: origin, size: size)
NSLog("Cell X:%@, Cell Y:%@",origin.x.description,origin.y.description)
return cell
}
计算尺寸的方法
func calculateCellSize() -> CGSize
{
//First check if we have valid values
let col = Int(widthInput.text!)
let row = Int(heightInput.text!)
if(col == nil || row == nil)
{
return CGSize(width: 48.0, height: 48.0)
}
//If there are more or equal amount of columns than rows
let columns = CGFloat(col!)
let rows = CGFloat(row!)
if(columns >= rows)
{
//Take the grid width
let gridWidth = drawCollection.bounds.size.width
//Calculate the width of the "pixels" that fit the width of the grid
var pixelWidth = gridWidth/columns
//Remember to substract the inset from the width
let drawLayout = drawCollection.collectionViewLayout as? UICollectionViewFlowLayout
pixelWidth -= (drawLayout?.sectionInset.left)! + 1/columns
return CGSize(width: pixelWidth, height: pixelWidth)
}
else
{
//Rows are more than columns
//Take the grid height as reference here
let gridHeight = drawCollection.bounds.size.height
//Calculate the height of the "pixels" that fit the height of the grid
var pixelHeight = gridHeight/rows
//Remember to substract the inset from the height
let drawLayout = drawCollection.collectionViewLayout as? UICollectionViewFlowLayout
pixelHeight -= (drawLayout?.sectionInset.top)! + 1/rows
return CGSize(width: pixelHeight, height: pixelHeight)
}
return CGSize(width: 48.0, height: 48.0)
}
出于调试原因,我在 cellforItemAtIndexPath 方法中放入了一个计数器,实际上我可以看到最后两行没有被调用。计数器以 185 结束,但理论上它应该被调用 248 次,实际上差异将显示它是 2*32 - 1(对于不均匀的 31)所以最后缺少的行....
我想到了几件事是什么原因,但似乎什么都不是:
- 单元格没有绘制在正确的位置(也就是在网格外)-> 至少不正确,因为该方法只被调用了 185 次。
- 单元格被计算为在网格之外,因此不会尝试由 UICollectionView 呈现 -> 仍然有可能,因为我不知道如何证明这一点。
- 存在 UICollectionView 可以绘制的最大元素数量(如果希望如此可配置)并且 31x8 已经超过了该数量 -> 仍然可能找不到任何相关信息。
所以总结:
是否可以显示网格中的所有元素(最大 32x64)?如果可以,我的实现有什么问题?
感谢大家的宝贵时间和回答!
您正在做大量不需要做的计算。此外,设置单元格的 .frame
是一个非常糟糕的主意。集合视图的一大要点是避免必须设置框架。
看看这个:
class GridCollectionViewController: UIViewController, UICollectionViewDataSource {
@IBOutlet weak var theCV: UICollectionView!
var numCols = 0
var numRows = 0
func updateCV() -> Void {
// subtract the number of colums (for the 1-pt spacing between cells), and divide by number of columns
let w = (theCV.frame.size.width - CGFloat((numCols - 1))) / CGFloat(numCols)
// subtract the number of rows (for the 1-pt spacing between rows), and divide by number of rows
let h = (theCV.frame.size.height - CGFloat((numRows - 1))) / CGFloat(numRows)
// get the smaller of the two values
let wh = min(w, h)
// set the cell size
if let layout = theCV.collectionViewLayout as? UICollectionViewFlowLayout {
layout.itemSize = CGSize(width: wh, height: wh)
}
// reload the collection view
theCV.reloadData()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// start with a 31x20 grid, just to see it
numCols = 31
numRows = 20
updateCV()
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return numRows
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return numCols
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = theCV.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
return cell
}
@IBAction func btnTap(_ sender: Any) {
// example of changing the number of rows/columns based on user action
numCols = 32
numRows = 64
updateCV()
}
}
您需要开发自己的 UICollectionViewLayout。使用这种方法,您可以获得人类可以想象的任何结果。
import UIKit
class CollectionLayout: UICollectionViewFlowLayout {
private var width: Int = 1
private var height: Int = 1
func set(width: Int, height: Int) {
guard width > 0, height > 0 else { return }
self.height = height
self.width = width
calculateItemSize()
}
private func calculateItemSize() {
guard let collectionView = collectionView else { return }
let size = collectionView.frame.size
var itemWidth = size.width / CGFloat(width)
// spacing is needed only if there're more than 2 items in a row
if width > 1 {
itemWidth -= minimumInteritemSpacing
}
var itemHeight = size.height / CGFloat(height)
if height > 1 {
itemHeight -= minimumLineSpacing
}
let edgeLength = min(itemWidth, itemHeight)
itemSize = CGSize(width: edgeLength, height: edgeLength)
}
// calculate origin for every item
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = super.layoutAttributesForItem(at: indexPath)
// calculate item position in the grid
let col = CGFloat(indexPath.row % width)
let row = CGFloat(Int(indexPath.row / width))
// don't forget to take into account 'minimumInteritemSpacing' and 'minimumLineSpacing'
let x = col * itemSize.width + col * minimumInteritemSpacing
let y = row * itemSize.height + row * minimumLineSpacing
// set new origin
attributes?.frame.origin = CGPoint(x: x, y: y)
return attributes
}
// accumulate all attributes
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var newAttributes = [UICollectionViewLayoutAttributes]()
for attribute in attributes {
if let newAttribute = layoutAttributesForItem(at: attribute.indexPath) {
newAttributes.append(newAttribute)
}
}
return newAttributes
}
}
将我们的布局设置为 UICollectionView
每次用户输入宽度和高度时更新集合视图:
@IBAction func onSetTapped(_ sender: Any) {
width = Int(widthTextField.text!)
height = Int(heightTextField.text!)
if let width = width, let height = height,
let layout = collectionView.collectionViewLayout as? CollectionLayout {
layout.set(width: width, height: height)
collectionView.reloadData()
}
}
我正在尝试创建一个简单的应用程序,其中可以为 UICollectionView 输入多列和多行。集合视图然后计算适合它的可能正方形的大小并绘制它们。 我想允许最大宽度为 32,高度为 64。滚动被禁用,因为整个网格应该一次显示。
例如,4x8 看起来像这样
8x4 看起来像这样
正如大家所见,它工作正常。问题伴随着更多的列 and/or 行。高达 30x8 一切都很好,但从 31 开始,8 行中只有 6 行被绘制。
所以我不明白为什么。以下是我用来计算所有内容的代码:
节数和行数:
func numberOfSections(in collectionView: UICollectionView) -> Int
{
let num = Int(heightInput.text!)
if(num != nil)
{
if(num! > 64)
{
return 64
}
return num!
}
return 8
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
let num = Int(widthInput.text!)
if(num != nil)
{
if(num! > 32)
{
return 32
}
return num!
}
return 4
}
indexPath 中项目的单元格
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let size = calculateCellSize()
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
var origin = cell.frame.origin
origin.x = 1+CGFloat(indexPath.row) + size.width*CGFloat(indexPath.row)
origin.y = 1+CGFloat(indexPath.section) + size.height*CGFloat(indexPath.section)
cell.frame = CGRect(origin: origin, size: size)
NSLog("Cell X:%@, Cell Y:%@",origin.x.description,origin.y.description)
return cell
}
计算尺寸的方法
func calculateCellSize() -> CGSize
{
//First check if we have valid values
let col = Int(widthInput.text!)
let row = Int(heightInput.text!)
if(col == nil || row == nil)
{
return CGSize(width: 48.0, height: 48.0)
}
//If there are more or equal amount of columns than rows
let columns = CGFloat(col!)
let rows = CGFloat(row!)
if(columns >= rows)
{
//Take the grid width
let gridWidth = drawCollection.bounds.size.width
//Calculate the width of the "pixels" that fit the width of the grid
var pixelWidth = gridWidth/columns
//Remember to substract the inset from the width
let drawLayout = drawCollection.collectionViewLayout as? UICollectionViewFlowLayout
pixelWidth -= (drawLayout?.sectionInset.left)! + 1/columns
return CGSize(width: pixelWidth, height: pixelWidth)
}
else
{
//Rows are more than columns
//Take the grid height as reference here
let gridHeight = drawCollection.bounds.size.height
//Calculate the height of the "pixels" that fit the height of the grid
var pixelHeight = gridHeight/rows
//Remember to substract the inset from the height
let drawLayout = drawCollection.collectionViewLayout as? UICollectionViewFlowLayout
pixelHeight -= (drawLayout?.sectionInset.top)! + 1/rows
return CGSize(width: pixelHeight, height: pixelHeight)
}
return CGSize(width: 48.0, height: 48.0)
}
出于调试原因,我在 cellforItemAtIndexPath 方法中放入了一个计数器,实际上我可以看到最后两行没有被调用。计数器以 185 结束,但理论上它应该被调用 248 次,实际上差异将显示它是 2*32 - 1(对于不均匀的 31)所以最后缺少的行....
我想到了几件事是什么原因,但似乎什么都不是:
- 单元格没有绘制在正确的位置(也就是在网格外)-> 至少不正确,因为该方法只被调用了 185 次。
- 单元格被计算为在网格之外,因此不会尝试由 UICollectionView 呈现 -> 仍然有可能,因为我不知道如何证明这一点。
- 存在 UICollectionView 可以绘制的最大元素数量(如果希望如此可配置)并且 31x8 已经超过了该数量 -> 仍然可能找不到任何相关信息。
所以总结:
是否可以显示网格中的所有元素(最大 32x64)?如果可以,我的实现有什么问题?
感谢大家的宝贵时间和回答!
您正在做大量不需要做的计算。此外,设置单元格的 .frame
是一个非常糟糕的主意。集合视图的一大要点是避免必须设置框架。
看看这个:
class GridCollectionViewController: UIViewController, UICollectionViewDataSource {
@IBOutlet weak var theCV: UICollectionView!
var numCols = 0
var numRows = 0
func updateCV() -> Void {
// subtract the number of colums (for the 1-pt spacing between cells), and divide by number of columns
let w = (theCV.frame.size.width - CGFloat((numCols - 1))) / CGFloat(numCols)
// subtract the number of rows (for the 1-pt spacing between rows), and divide by number of rows
let h = (theCV.frame.size.height - CGFloat((numRows - 1))) / CGFloat(numRows)
// get the smaller of the two values
let wh = min(w, h)
// set the cell size
if let layout = theCV.collectionViewLayout as? UICollectionViewFlowLayout {
layout.itemSize = CGSize(width: wh, height: wh)
}
// reload the collection view
theCV.reloadData()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// start with a 31x20 grid, just to see it
numCols = 31
numRows = 20
updateCV()
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return numRows
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return numCols
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = theCV.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
return cell
}
@IBAction func btnTap(_ sender: Any) {
// example of changing the number of rows/columns based on user action
numCols = 32
numRows = 64
updateCV()
}
}
您需要开发自己的 UICollectionViewLayout。使用这种方法,您可以获得人类可以想象的任何结果。
import UIKit
class CollectionLayout: UICollectionViewFlowLayout {
private var width: Int = 1
private var height: Int = 1
func set(width: Int, height: Int) {
guard width > 0, height > 0 else { return }
self.height = height
self.width = width
calculateItemSize()
}
private func calculateItemSize() {
guard let collectionView = collectionView else { return }
let size = collectionView.frame.size
var itemWidth = size.width / CGFloat(width)
// spacing is needed only if there're more than 2 items in a row
if width > 1 {
itemWidth -= minimumInteritemSpacing
}
var itemHeight = size.height / CGFloat(height)
if height > 1 {
itemHeight -= minimumLineSpacing
}
let edgeLength = min(itemWidth, itemHeight)
itemSize = CGSize(width: edgeLength, height: edgeLength)
}
// calculate origin for every item
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = super.layoutAttributesForItem(at: indexPath)
// calculate item position in the grid
let col = CGFloat(indexPath.row % width)
let row = CGFloat(Int(indexPath.row / width))
// don't forget to take into account 'minimumInteritemSpacing' and 'minimumLineSpacing'
let x = col * itemSize.width + col * minimumInteritemSpacing
let y = row * itemSize.height + row * minimumLineSpacing
// set new origin
attributes?.frame.origin = CGPoint(x: x, y: y)
return attributes
}
// accumulate all attributes
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var newAttributes = [UICollectionViewLayoutAttributes]()
for attribute in attributes {
if let newAttribute = layoutAttributesForItem(at: attribute.indexPath) {
newAttributes.append(newAttribute)
}
}
return newAttributes
}
}
将我们的布局设置为 UICollectionView
每次用户输入宽度和高度时更新集合视图:
@IBAction func onSetTapped(_ sender: Any) {
width = Int(widthTextField.text!)
height = Int(heightTextField.text!)
if let width = width, let height = height,
let layout = collectionView.collectionViewLayout as? CollectionLayout {
layout.set(width: width, height: height)
collectionView.reloadData()
}
}