[英]Custom collection view layout crashes
I've created a custom data grid. 我创建了一个自定义数据网格。 It's based on collection view with custom layout. 它基于具有自定义布局的集合视图。 The layout modifies the first section and row attributes making them sticky, so when the user scrolls other rows and sections should go under the sticky ones. 该布局修改了第一节和行属性,使其具有粘性,因此,当用户滚动其他行和节时,它们应位于粘性之下。 The idea for this layout is not mine, I've just adopted it. 这种布局的想法不是我的,我只是采用了它。 ( I can't give the credits for the real creator, in my research I found so many variations of the layout that I'm not sure which is the original one ). ( 我不能为真正的创作者付出功劳,在我的研究中,我发现布局的变化太多,因此我不确定哪一个是原始布局 )。
Unfortunately I'm facing a problem with it. 不幸的是,我面临着一个问题。 I'm getting a crash when scrolling: 滚动时出现崩溃:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no UICollectionViewLayoutAttributes instance for -layoutAttributesForItemAtIndexPath: ***由于未捕获的异常“ NSInternalInconsistencyException”而终止应用程序,原因:“-layoutAttributesForItemAtIndexPath没有UICollectionViewLayoutAttributes实例:
Despite the message I think that the real problem is in layoutAttributesForElements
method. 尽管有该消息,但我认为真正的问题出在layoutAttributesForElements
方法中。 I've read some threads with a similar problem, but the only working solution is to return all cached attributes, no matter of the passed rectangle. 我已经阅读了一些具有类似问题的线程,但是唯一可行的解决方案是返回所有缓存的属性,无论传递的矩形如何。 I just don't like quick and dirty solutions like this. 我只是不喜欢这种快速又肮脏的解决方案。 I would really appreciate any ideas/solutions you can give me. 我真的很感激您能给我的任何想法/解决方案。
The whole project is here . 整个项目在这里 。 However the most important is the layout so for convenience here it is: 但是,最重要的是布局,因此为了方便起见,它是:
class GridViewLayout: UICollectionViewLayout {
//MARK: - Setup
private var isInitialized: Bool = false
//MARK: - Attributes
var attributesList: [[UICollectionViewLayoutAttributes]] = []
//MARK: - Size
private static let defaultGridViewItemHeight: CGFloat = 47
private static let defaultGridViewItemWidth: CGFloat = 160
static let defaultGridViewRowHeaderWidth: CGFloat = 200
static let defaultGridViewColumnHeaderHeight: CGFloat = 80
static let defaultGridViewItemSize: CGSize =
CGSize(width: defaultGridViewItemWidth, height: defaultGridViewItemHeight)
// This is regular cell size
var itemSize: CGSize = defaultGridViewItemSize
// Row Header Size
var rowHeaderSize: CGSize =
CGSize(width: defaultGridViewRowHeaderWidth, height: defaultGridViewItemHeight)
// Column Header Size
var columnHeaderSize: CGSize =
CGSize(width: defaultGridViewItemWidth, height: defaultGridViewColumnHeaderHeight)
var contentSize : CGSize!
//MARK: - Layout
private var columnsCount: Int = 0
private var rowsCount: Int = 0
private var includesRowHeader: Bool = false
private var includesColumnHeader: Bool = false
override func prepare() {
super.prepare()
rowsCount = collectionView!.numberOfSections
if rowsCount == 0 { return }
columnsCount = collectionView!.numberOfItems(inSection: 0)
// make header row and header column sticky if needed
if self.attributesList.count > 0 {
for section in 0..<rowsCount {
for index in 0..<columnsCount {
if section != 0 && index != 0 {
continue
}
let attributes : UICollectionViewLayoutAttributes =
layoutAttributesForItem(at: IndexPath(forRow: section, inColumn: index))!
if includesColumnHeader && section == 0 {
var frame = attributes.frame
frame.origin.y = collectionView!.contentOffset.y
attributes.frame = frame
}
if includesRowHeader && index == 0 {
var frame = attributes.frame
frame.origin.x = collectionView!.contentOffset.x
attributes.frame = frame
}
}
}
return // no need for futher calculations
}
// Read once from delegate
if !isInitialized {
if let delegate = collectionView!.delegate as? UICollectionViewDelegateGridLayout {
// Calculate Item Sizes
let indexPath = IndexPath(forRow: 0, inColumn: 0)
let _itemSize = delegate.collectionView(collectionView!,
layout: self,
sizeForItemAt: indexPath)
let width = delegate.rowHeaderWidth(in: collectionView!,
layout: self)
let _rowHeaderSize = CGSize(width: width, height: _itemSize.height)
let height = delegate.columnHeaderHeight(in: collectionView!,
layout: self)
let _columnHeaderSize = CGSize(width: _itemSize.width, height: height)
if !__CGSizeEqualToSize(_itemSize, itemSize) {
itemSize = _itemSize
}
if !__CGSizeEqualToSize(_rowHeaderSize, rowHeaderSize) {
rowHeaderSize = _rowHeaderSize
}
if !__CGSizeEqualToSize(_columnHeaderSize, columnHeaderSize) {
columnHeaderSize = _columnHeaderSize
}
// Should enable sticky row and column headers
includesRowHeader = delegate.shouldIncludeHeaderRow(in: collectionView!)
includesColumnHeader = delegate.shouldIncludeHeaderColumn(in: collectionView!)
}
isInitialized = true
}
var column = 0
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var contentWidth : CGFloat = 0
var contentHeight : CGFloat = 0
for section in 0..<rowsCount {
var sectionAttributes: [UICollectionViewLayoutAttributes] = []
for index in 0..<columnsCount {
var _itemSize: CGSize = .zero
switch (section, index) {
case (0, 0):
switch (includesRowHeader, includesColumnHeader) {
case (true, true):
_itemSize = CGSize(width: rowHeaderSize.width, height: columnHeaderSize.height)
case (false, true): _itemSize = columnHeaderSize
case (true, false): _itemSize = rowHeaderSize
default: _itemSize = itemSize
}
case (0, _):
if includesColumnHeader {
_itemSize = columnHeaderSize
} else {
_itemSize = itemSize
}
case (_, 0):
if includesRowHeader {
_itemSize = rowHeaderSize
} else {
_itemSize = itemSize
}
default: _itemSize = itemSize
}
let indexPath = IndexPath(forRow: section, inColumn: index)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset,
y: yOffset,
width: _itemSize.width,
height: _itemSize.height).integral
// allow others cells to go under
if section == 0 && index == 0 { // top-left cell
attributes.zIndex = 1024
} else if section == 0 || index == 0 {
attributes.zIndex = 1023 // any ohter header cell
}
// sticky part - probably just in case here
if includesColumnHeader && section == 0 {
var frame = attributes.frame
frame.origin.y = collectionView!.contentOffset.y
attributes.frame = frame
}
if includesRowHeader && index == 0 {
var frame = attributes.frame
frame.origin.x = collectionView!.contentOffset.x
attributes.frame = frame
}
sectionAttributes.append(attributes)
xOffset += _itemSize.width
column += 1
if column == columnsCount {
if xOffset > contentWidth {
contentWidth = xOffset
}
column = 0
xOffset = 0
yOffset += _itemSize.height
}
}
attributesList.append(sectionAttributes)
}
let attributes = self.attributesList.last!.last!
contentHeight = attributes.frame.origin.y + attributes.frame.size.height
self.contentSize = CGSize(width: contentWidth,
height: contentHeight)
}
override var collectionViewContentSize: CGSize {
return self.contentSize
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
var curLayoutAttribute: UICollectionViewLayoutAttributes? = nil
if indexPath.section < self.attributesList.count {
let sectionAttributes = self.attributesList[indexPath.section]
if indexPath.row < sectionAttributes.count {
curLayoutAttribute = sectionAttributes[indexPath.row]
}
}
return curLayoutAttribute
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes: [UICollectionViewLayoutAttributes] = []
for section in self.attributesList {
let filteredArray = section.filter({ (evaluatedObject) -> Bool in
return rect.intersects(evaluatedObject.frame)
})
attributes.append(contentsOf: filteredArray)
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
//MARK: - Moving
override func layoutAttributesForInteractivelyMovingItem(at indexPath: IndexPath,
withTargetPosition position: CGPoint) -> UICollectionViewLayoutAttributes {
guard let dest = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as? UICollectionViewLayoutAttributes else { return UICollectionViewLayoutAttributes() }
dest.transform = CGAffineTransform(scaleX: 1.4, y: 1.4)
dest.alpha = 0.8
dest.center = position
return dest
}
override func invalidationContext(forInteractivelyMovingItems targetIndexPaths: [IndexPath],
withTargetPosition targetPosition: CGPoint,
previousIndexPaths: [IndexPath],
previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forInteractivelyMovingItems: targetIndexPaths,
withTargetPosition: targetPosition,
previousIndexPaths: previousIndexPaths,
previousPosition: previousPosition)
collectionView!.dataSource?.collectionView?(collectionView!,
moveItemAt: previousIndexPaths[0],
to: targetIndexPaths[0])
return context
}
}
Implement layoutAttributesForItemAtIndexPath
. 实现layoutAttributesForItemAtIndexPath
。 As per the documentation, "Subclasses must override this method and use it to return layout information for items in the collection view. ". 根据文档,“子类必须重写此方法,并使用它返回集合视图中项目的布局信息。”。
In my experience this method is normally not called when running in the simulator but can be called on the device. 以我的经验,在模拟器中运行时通常不会调用此方法,但可以在设备上调用该方法。 YMMV. 因人而异。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.