swift中的UICollectionView粘性标题
Posted
技术标签:
【中文标题】swift中的UICollectionView粘性标题【英文标题】:UICollectionView sticky header in swift 【发布时间】:2014-12-05 12:48:59 【问题描述】:我正在尝试创建一个粘性补充标题,它始终保持在顶部并且不会响应滚动事件。到目前为止,我发现的解决方案仍然对弹跳滚动做出反应,并使用自定义 flowLayout 进行修复,这也可能是我的问题的修复。
我想要这样的原因是标题在其他地方使用并且应该是可重用的。我希望这可以通过这种方式解决,并且我不必创建单独的视图。
因为我在 Swift 中做这件事,所以在 Swift 中有一个例子会很棒。
【问题讨论】:
【参考方案1】:ios 9 + 最简单的解决方案,因为它不需要编写 UICollectionViewFlowLayout 的子类。
在带有 collectionView 的 viewController 的 viewDidLoad 中使用以下代码:
let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout // casting is required because UICollectionViewLayout doesn't offer header pin. Its feature of UICollectionViewFlowLayout
layout?.sectionHeadersPinToVisibleBounds = true
@Antoine 也暗示了这一点。
【讨论】:
这个方法有效,但是我想知道为什么sectionheader遇到footer时会被推上去 这太棒了!最佳解决方案。 绝对是最直接且易于使用的解决方案。 现在可以放弃 iOS 8,这个答案太棒了:D 这太棒了!【参考方案2】:我找到的最终解决方案:
使用这个自定义流布局可以修复这个粘性标题:
class StickyHeaderCollectionViewFlowLayout: UICollectionViewFlowLayout
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]?
var superAttributes: [UICollectionViewLayoutAttributes]? = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]
if superAttributes == nil
// If superAttributes couldn't cast, return
return super.layoutAttributesForElementsInRect(rect)
let contentOffset = collectionView!.contentOffset
var missingSections = NSMutableIndexSet()
for layoutAttributes in superAttributes!
if (layoutAttributes.representedElementCategory == .Cell)
if let indexPath = layoutAttributes.indexPath
missingSections.addIndex(layoutAttributes.indexPath.section)
for layoutAttributes in superAttributes!
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
if let indexPath = layoutAttributes.indexPath
missingSections.removeIndex(indexPath.section)
missingSections.enumerateIndexesUsingBlock idx, stop in
let indexPath = NSIndexPath(forItem: 0, inSection: idx)
if let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
superAttributes!.append(layoutAttributes)
for layoutAttributes in superAttributes!
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
let section = layoutAttributes.indexPath!.section
let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)
let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)!
let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)!
let (firstCellAttributes: UICollectionViewLayoutAttributes, lastCellAttributes: UICollectionViewLayoutAttributes) =
if (self.collectionView!.numberOfItemsInSection(section) > 0)
return (
self.layoutAttributesForItemAtIndexPath(firstCellIndexPath),
self.layoutAttributesForItemAtIndexPath(lastCellIndexPath))
else
return (
self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath),
self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath))
()
let headerHeight = CGRectGetHeight(layoutAttributes.frame)
var origin = layoutAttributes.frame.origin
origin.y = min(contentOffset.y, (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
// Uncomment this line for normal behaviour:
// origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
layoutAttributes.zIndex = 1024
layoutAttributes.frame = CGRect(origin: origin, size: layoutAttributes.frame.size)
return superAttributes
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool
return true
要创建一个标题像传统一样粘性的布局,请更改此行:
origin.y = min(contentOffset.y, (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight))
到这一行:
origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight))
希望这对其他人有用!
更新
已更新以修复崩溃(感谢 Robert Atkins!)以及 Swift 1.2 的一些更新
tvOS 和 iOS 9
tvOS 和 iOS 9 引入了可以使用的属性sectionHeadersPinToVisibleBounds
【讨论】:
我在firstCellAttrs = ...
行上遇到 EXC_ARITHMETIC 崩溃,可能与 ***.com/questions/24616797/… 相关?
显然这对我不起作用:Xcode 6.1.1 和 iOS 8 :-( 有什么想法吗?你能重新测试一下吗?标题应该像github.com/jamztang/CSStickyHeaderFlowLayout 一样贴在顶部,对吗?
不适合我的崩溃在这里self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath))
@NickWargnier 确保您的部分中至少有一个单元格。需要在上面的代码中添加修复
> sectionHeadersPinToVisibleBounds 你救了我的命,伙计!谢谢!所有其他解决方案都非常缓慢。【参考方案3】:
使用 swift 2.0 为我工作
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?
var superAttributes:NSMutableArray = NSMutableArray(array: super.layoutAttributesForElementsInRect(rect)!) as NSMutableArray
let contentOffset = collectionView!.contentOffset
var missingSections = NSMutableIndexSet()
for layoutAttributes in superAttributes
if (layoutAttributes.representedElementCategory == .Cell)
if let indexPath = layoutAttributes.indexPath
missingSections.addIndex(layoutAttributes.indexPath.section)
for layoutAttributes in superAttributes
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
if let indexPath = layoutAttributes.indexPath
missingSections.removeIndex(indexPath.section)
missingSections.enumerateIndexesUsingBlock idx, stop in
let indexPath = NSIndexPath(forItem: 0, inSection: idx)
let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
superAttributes.addObject(layoutAttributes!)
for la in superAttributes
let layoutAttributes = la as! UICollectionViewLayoutAttributes;
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
let section = layoutAttributes.indexPath.section
let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)
let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)
let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)
var firstCellAttributes:UICollectionViewLayoutAttributes
var lastCellAttributes:UICollectionViewLayoutAttributes
if (self.collectionView!.numberOfItemsInSection(section) > 0)
firstCellAttributes = self.layoutAttributesForItemAtIndexPath(firstCellIndexPath)!
lastCellAttributes = self.layoutAttributesForItemAtIndexPath(lastCellIndexPath)!
else
firstCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath)!
lastCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath)!
let headerHeight = CGRectGetHeight(layoutAttributes.frame)
var origin = layoutAttributes.frame.origin
origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
;
layoutAttributes.zIndex = 1024;
layoutAttributes.frame = CGRect(origin: origin, size: layoutAttributes.frame.size)
return NSArray(array: superAttributes) as? [UICollectionViewLayoutAttributes]
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool
return true
【讨论】:
此代码仅适用于垂直滚动,您可以使其适用于水平滚动吗?和顶部的部分标题?【参考方案4】:按照this gist 中的建议,我通过测试空部分来修复我的崩溃问题。我还添加了几个if let
s 以获得额外的 Swift 风格点 ;-)。现在这对我有用:
class StickyHeaderCollectionViewFlowLayout: UICollectionViewFlowLayout
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]?
var answer: [UICollectionViewLayoutAttributes] = super.layoutAttributesForElementsInRect(rect)! as [UICollectionViewLayoutAttributes]
let contentOffset = collectionView!.contentOffset
var missingSections = NSMutableIndexSet()
for layoutAttributes in answer
if (layoutAttributes.representedElementCategory == .Cell)
if let indexPath = layoutAttributes.indexPath
missingSections.addIndex(layoutAttributes.indexPath.section)
for layoutAttributes in answer
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
if let indexPath = layoutAttributes.indexPath
missingSections.removeIndex(indexPath.section)
missingSections.enumerateIndexesUsingBlock idx, stop in
let indexPath = NSIndexPath(forItem: 0, inSection: idx)
if let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
answer.append(layoutAttributes)
for layoutAttributes in answer
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
let section = layoutAttributes.indexPath!.section
let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)
let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)!
let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)!
let (firstCellAttributes: UICollectionViewLayoutAttributes, lastCellAttributes: UICollectionViewLayoutAttributes) =
if (self.collectionView!.numberOfItemsInSection(section) > 0)
return (
self.layoutAttributesForItemAtIndexPath(firstCellIndexPath),
self.layoutAttributesForItemAtIndexPath(lastCellIndexPath))
else
return (
self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath),
self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath))
()
let headerHeight = CGRectGetHeight(layoutAttributes.frame)
var origin = layoutAttributes.frame.origin
origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
layoutAttributes.zIndex = 1024
layoutAttributes.frame = CGRect(origin: origin, size: layoutAttributes.frame.size)
return answer
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool
return true
【讨论】:
谢谢!我已经用你更新的部分代码更新了我的答案。 谢谢!仅供参考,在 Swift 1.2 中,layoutAttributesForElementsIntRect 的第一行现在需要一个“as!” var 答案:[UICollectionViewLayoutAttributes] = super.layoutAttributesForElementsInRect(rect)!作为! [UICollectionViewLayoutAttributes] 我在 swift 2.0 中的let (firstCellAttributes: UICollectionViewLayoutAttributes, lastCellAttributes: UICollectionViewLayoutAttributes) =
处遇到“定义与先前值冲突”【参考方案5】:
Irina 答案的 Cleaner Swift 2.3 测试版
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?
guard var superAttributes = super.layoutAttributesForElementsInRect(rect) else
return super.layoutAttributesForElementsInRect(rect)
let contentOffset = collectionView!.contentOffset
let missingSections = NSMutableIndexSet()
for layoutAttributes in superAttributes
if (layoutAttributes.representedElementCategory == .Cell)
missingSections.addIndex(layoutAttributes.indexPath.section)
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
missingSections.removeIndex(layoutAttributes.indexPath.section)
missingSections.enumerateIndexesUsingBlock idx, stop in
let indexPath = NSIndexPath(forItem: 0, inSection: idx)
if let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
superAttributes.append(layoutAttributes)
for layoutAttributes in superAttributes
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
let section = layoutAttributes.indexPath.section
let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)
let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)
let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)
var firstCellAttributes:UICollectionViewLayoutAttributes
var lastCellAttributes:UICollectionViewLayoutAttributes
if (self.collectionView!.numberOfItemsInSection(section) > 0)
firstCellAttributes = self.layoutAttributesForItemAtIndexPath(firstCellIndexPath)!
lastCellAttributes = self.layoutAttributesForItemAtIndexPath(lastCellIndexPath)!
else
firstCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath)!
lastCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath)!
let headerHeight = CGRectGetHeight(layoutAttributes.frame)
var origin = layoutAttributes.frame.origin
origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
;
layoutAttributes.zIndex = 1024;
layoutAttributes.frame = CGRect(origin: origin, size: layoutAttributes.frame.size)
return superAttributes
【讨论】:
【参考方案6】:根据 GregP 的回答测试了 Swift 2.2 版本。 Greg 的代码在 lastCellIndexPath 处抛出了一个 unwrap optional 错误,因为节数最初为零。所以我移动了 numberOfItemsInSection > 0 检查。
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?
let superAttributes:NSMutableArray = NSMutableArray(array: super.layoutAttributesForElementsInRect(rect)!) as NSMutableArray
let contentOffset = collectionView!.contentOffset
let missingSections = NSMutableIndexSet()
for layoutAttributes in superAttributes
if (layoutAttributes.representedElementCategory == .Cell)
if let _ = layoutAttributes.indexPath
missingSections.addIndex(layoutAttributes.indexPath.section)
for layoutAttributes in superAttributes
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
if let indexPath = layoutAttributes.indexPath
missingSections.removeIndex(indexPath.section)
missingSections.enumerateIndexesUsingBlock idx, stop in
let indexPath = NSIndexPath(forItem: 0, inSection: idx)
let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
superAttributes.addObject(layoutAttributes!)
for la in superAttributes
let layoutAttributes = la as! UICollectionViewLayoutAttributes;
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
let section = layoutAttributes.indexPath.section
let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)
if numberOfItemsInSection > 0
let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)
let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)
var firstCellAttributes:UICollectionViewLayoutAttributes
var lastCellAttributes:UICollectionViewLayoutAttributes
firstCellAttributes = self.layoutAttributesForItemAtIndexPath(firstCellIndexPath)!
lastCellAttributes = self.layoutAttributesForItemAtIndexPath(lastCellIndexPath)!
let headerHeight = CGRectGetHeight(layoutAttributes.frame)
var origin = layoutAttributes.frame.origin
origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight));
layoutAttributes.zIndex = 1024;
layoutAttributes.frame = CGRect(origin: origin, size: layoutAttributes.frame.size)
return NSArray(array: superAttributes) as? [UICollectionViewLayoutAttributes]
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool
return true
【讨论】:
【参考方案7】:对于想要在组合布局上使用置顶标题的人:
(根据需要更改高度值)
let headerKind = UICollectionView.elementKindSectionHeader
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(100))
let headerElement = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: headerKind, alignment: .top)
// for sticky header
headerElement.pinToVisibleBounds = true
section.boundarySupplementaryItems = [headerElement]
【讨论】:
【参考方案8】:如果您只有一个部分,这是一个简单的解决方案。
class StickyHeaderLayout: UICollectionViewFlowLayout
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool
return true
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]?
var attributes = super.layoutAttributesForElementsInRect(rect)! as! [UICollectionViewLayoutAttributes]
let offset = collectionView?.contentOffset
for attrs in attributes
if attrs.representedElementKind == nil
let indexPath = NSIndexPath(forItem: 0, inSection: attrs.indexPath.section)
let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
attributes.append(layoutAttributes)
for attrs in attributes
if attrs.representedElementKind == nil
continue
if attrs.representedElementKind == UICollectionElementKindSectionHeader
var headerRect = attrs.frame
headerRect.size.height = headerHeight
headerRect.origin.y = offset!.y
attrs.frame = headerRect
attrs.zIndex = 1024
break
return attributes
【讨论】:
【参考方案9】:Swift 3 版本试图避免 ! 的意义所在。
class StickyHeaderCollectionViewFlowLayout: UICollectionViewFlowLayout
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
guard var superAttributes = super.layoutAttributesForElements(in: rect), let collectionView = collectionView else
return super.layoutAttributesForElements(in: rect)
let collectionViewTopY = collectionView.contentOffset.y + collectionView.contentInset.top
let contentOffset = CGPoint(x: 0, y: collectionViewTopY)
let missingSections = NSMutableIndexSet()
superAttributes.forEach layoutAttributes in
if layoutAttributes.representedElementCategory == .cell && layoutAttributes.representedElementKind != UICollectionElementKindSectionHeader
missingSections.add(layoutAttributes.indexPath.section)
missingSections.enumerate(using: idx, stop in
let indexPath = IndexPath(item: 0, section: idx)
if let layoutAttributes = self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: indexPath)
superAttributes.append(layoutAttributes)
)
for layoutAttributes in superAttributes
if let representedElementKind = layoutAttributes.representedElementKind
if representedElementKind == UICollectionElementKindSectionHeader
let section = layoutAttributes.indexPath.section
let numberOfItemsInSection = collectionView.numberOfItems(inSection: section)
let firstCellIndexPath = IndexPath(item: 0, section: section)
let lastCellIndexPath = IndexPath(item: max(0, (numberOfItemsInSection - 1)), section: section)
let cellAttributes:(first: UICollectionViewLayoutAttributes, last: UICollectionViewLayoutAttributes) =
if (collectionView.numberOfItems(inSection: section) > 0)
return (
self.layoutAttributesForItem(at: firstCellIndexPath)!,
self.layoutAttributesForItem(at: lastCellIndexPath)!)
else
return (
self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: firstCellIndexPath)!,
self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionFooter, at: lastCellIndexPath)!)
()
let headerHeight = layoutAttributes.frame.height
var origin = layoutAttributes.frame.origin
// This line makes only one header visible fixed at the top
// origin.y = min(contentOffset.y, cellAttributes.last.frame.maxY - headerHeight)
// Uncomment this line for normal behaviour:
origin.y = min(max(contentOffset.y, cellAttributes.first.frame.minY - headerHeight), cellAttributes.last.frame.maxY - headerHeight)
layoutAttributes.zIndex = 1024
layoutAttributes.frame = CGRect(origin: origin, size: layoutAttributes.frame.size)
return superAttributes
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool
return true
【讨论】:
【参考方案10】:我在我的项目中使用它。希望对您有所帮助。
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -> CGSize
return CGSizeMake(UIScreen.mainScreen().bounds.width, 40)
当然。你可以返回任何你想要的尺寸。
【讨论】:
以上是关于swift中的UICollectionView粘性标题的主要内容,如果未能解决你的问题,请参考以下文章
带有粘性水平指示器的 UICollectionView Swift 3.0
具有特定于部分的布局和粘性标题的 UICollectionView
如何在顶部 UICollectionView 上制作粘性标题