ios 快速的 UICollectionView 粘性标题
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/27316263/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
UICollectionView sticky header in swift
提问by Antoine
I'm trying to create a sticky supplementary header, which stays on top all the time and won't response to scrolling events. The solutions I found so far still react on bounch scrolling and are fixed using a custom flowLayout, which will probably be the fix for mine issue as well.
我正在尝试创建一个粘性补充标题,它始终处于顶部并且不会响应滚动事件。到目前为止,我发现的解决方案仍然对反弹滚动做出反应,并使用自定义 flowLayout 进行修复,这也可能是我的问题的解决方案。
The reason I want it this way is that the header is used on other places and should be reusable. I'm hoping this could be solved this way and I don't have to create a separated view.
我想要这种方式的原因是标题在其他地方使用并且应该是可重用的。我希望这可以通过这种方式解决,而且我不必创建单独的视图。
As I'm doing this in Swift, it would be great to have an example in Swift.
因为我是在 Swift 中做这件事的,所以在 Swift 中有一个例子会很棒。
采纳答案by Antoine
The final solution I found:
我找到的最终解决方案:
Using this custom flow layout it was possible to fix this sticky header:
使用此自定义流布局可以修复此粘性标题:
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
}
}
To create a layout where the headers are sticky like traditional, change this line:
要创建标题像传统一样粘性的布局,请更改以下行:
origin.y = min(contentOffset.y, (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight))
to this line:
到这一行:
origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight))
Hoping this is useful for others!
希望这对其他人有用!
Update
更新
Updated to fix a crash (thanks to Robert Atkins!) and some updates to Swift 1.2
更新以修复崩溃(感谢 Robert Atkins!)以及对 Swift 1.2 的一些更新
tvOS & iOS 9
tvOS 和 iOS 9
tvOS and iOS 9 introduced the property sectionHeadersPinToVisibleBounds
which can be used
tvOS 和 iOS 9 引入了sectionHeadersPinToVisibleBounds
可以使用的属性
回答by Dari
Simplest solution for iOS 9 + as it doesn't need of writing subclass of UICollectionViewFlowLayout.
iOS 9 + 最简单的解决方案,因为它不需要编写 UICollectionViewFlowLayout 的子类。
In viewDidLoad of viewController with collectionView use following code:
在带有 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
It is hinted by @Antoine also.
@Antoine 也暗示了这一点。
回答by Irina Didkovskaya
works for me with swift 2.0
使用 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
}
回答by GregP
Cleaner Swift 2.3 tested version of Irina's answer
Cleaner Swift 2.3 测试版 Irina 的答案
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
}
回答by Ross
Swift 2.2 version tested based on GregP's answer. Greg's code was throwing an unwrap optional error at lastCellIndexPathbecause the number of section is initially zero. So I moved the numberOfItemsInSection > 0check up.
Swift 2.2 版本基于 GregP 的回答进行了测试。Greg 的代码在lastCellIndexPath抛出了一个unwrap可选错误,因为部分的数量最初为零。所以我移动了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
}
回答by Robert Atkins
I fixed my crash by testing for testing for empty sections as suggested in this gist. I also added a couple of if let
s for extra Swift style points ;-). This now works for me:
我按照本要点中的建议通过测试空部分的测试来修复我的崩溃。我还添加了几个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
}
}
回答by Nick Wargnier
Here is a simple solution that works if you only have one section.
如果您只有一个部分,这里有一个简单的解决方案。
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
}
}
回答by user12345625
Swift 3 version trying to avoid !'s where it made sense.
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
}
}
回答by David
i use this in my project. Hope it helps.
我在我的项目中使用它。希望能帮助到你。
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -> CGSize
{
return CGSizeMake(UIScreen.mainScreen().bounds.width, 40)
}
of course. you can return whatever size you want.
当然。你可以返回任何你想要的尺寸。