自 iOS 8 起
这个答案是在 iOS 8 种子之前写的。它提到了希望在 iOS 7 中不完全存在的功能,并提供了解决方法。此功能现在确实存在并且有效。另一个答案,目前在页面下方,描述了an approach for iOS 8。
讨论
首先 - 对于任何类型的优化,真正重要的谨慎是首先分析并了解您的性能瓶颈到底在哪里。
我查看了UICollectionViewLayoutInvalidationContext 并同意它似乎可以提供所需的功能。在关于这个问题的 cmets 中,我描述了我试图让它发挥作用的尝试。我现在怀疑虽然它允许您删除布局重新计算,但它不会帮助您避免对内容单元格进行布局更改。就我而言,布局计算并不是特别昂贵,但我确实想避免框架将布局更改应用于简单的滚动单元格(其中我有很多),而只将它们应用于“特殊”单元格。
实施总结
鉴于没有按照 Apple 的意图去做,我作弊了。我使用 2 UICollectionView 实例。我在背景视图上有正常的滚动内容,在第二个前景视图上有标题。视图的布局指定背景视图不会因边界更改而失效,而前景视图会。
实现细节
为了使这项工作正常进行,您需要做一些不明显的事情,而且我还有一些实施技巧,我发现这些技巧让我的生活更轻松。我将通过这个并提供从我的应用程序中提取的代码片段。我不会在这里提供完整的解决方案,但我会提供您需要的所有部分。
UICollectionView 有一个backgroundView 属性。
我在UICollectionViewController 的viewDidLoad 方法中创建了背景视图。至此,视图控制器的collectionView 属性中已经有一个UICollectionView 实例。这将是前景视图,将用于具有特殊滚动行为的项目,例如固定。
我创建了第二个UICollectionView 实例并将其设置为前台集合视图的backgroundView 属性。我将背景设置为也使用UICollectionViewController 子类作为它的数据源和委托。我禁用了背景视图上的用户交互,因为否则它似乎会获取所有事件。如果您想要选择等,您可能需要比这更微妙的行为:
…
UICollectionView *const foregroundCollectionView = [self collectionView];
UICollectionView *const backgroundCollectionView = [[UICollectionView alloc] initWithFrame: [foregroundCollectionView frame] collectionViewLayout: [[STGridLayout alloc] init]];
[backgroundCollectionView setDataSource: self];
[backgroundCollectionView setDelegate: self];
[backgroundCollectionView setUserInteractionEnabled: NO];
[foregroundCollectionView setBackgroundView: backgroundCollectionView];
[(STGridLayout*)[backgroundCollectionView collectionViewLayout] setInvalidateLayoutForBoundsChange: NO];
[(STGridLayout*)[foregroundCollectionView collectionViewLayout] setInvalidateLayoutForBoundsChange: YES];
…
总而言之——在这一点上,我们有两个相互叠加的集合视图。后面的将用于静态内容。前面的将用于固定内容等。它们都指向同一个 UICollectionViewController 作为它们的委托和数据源。
STGridLayout 上的invalidateLayoutForBoundsChange 属性是我添加到自定义布局中的。当调用-(BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 时,布局只会返回它。
这两个视图的更多设置在我的例子中看起来像这样:
for(UICollectionView *collectionView in @[foregroundCollectionView, backgroundCollectionView])
{
// Configure reusable views.
[STCollectionViewStaticCVCell registerForReuseInView: collectionView];
[STBlockRenderedCollectionViewCell registerForReuseInView: collectionView];
}
registerForReuseInView: 方法是按类别添加到UICollectionReusableView 的东西,以及dequeueFromView:。这些代码在答案的末尾。
viewDidLoad 的下一篇文章是这种方法唯一令人头疼的问题。
当您拖动前景视图时,您需要背景视图随之滚动。稍后我将展示此代码:它只是将前景视图的contentOffset 镜像到背景视图。但是,您可能希望滚动视图能够在内容的边缘“反弹”。 UICollectionView 似乎会在以编程方式设置时clamp contentOffset,这样内容就不会脱离UICollectionView 的边界。如果不采取补救措施,只有前景的粘性元素会反弹,这看起来很可怕。但是,将以下内容添加到您的 viewDidLoad 将解决此问题:
CGSize size = [foregroundCollectionView bounds].size;
[backgroundCollectionView setContentInset: UIEdgeInsetsMake(size.width, size.height, size.width, size.height)];
不幸的是,此修复将意味着当您在屏幕上显示视图时,背景的内容偏移量将与前景不匹配。要解决此问题,您需要实现:
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear: animated];
UICollectionView *const foregroundCollectionView = [self collectionView];
UICollectionView *const backgroundCollectionView = (UICollectionView *)[foregroundCollectionView backgroundView];
[backgroundCollectionView setContentOffset: [foregroundCollectionView contentOffset]];
}
我确信在 viewDidAppear: 中执行此操作会更有意义,但这对我不起作用。
您需要做的最后一件重要的事情是让背景滚动与前景保持同步,如下所示:
-(void) scrollViewDidScroll:(UIScrollView *const)scrollView
{
UICollectionView *const collectionView = [self collectionView];
if(scrollView == collectionView)
{
const CGPoint contentOffset = [collectionView contentOffset];
UIScrollView *const backgroundView = (UIScrollView*)[collectionView backgroundView];
[backgroundView setContentOffset: contentOffset];
}
}
实施技巧
这些是帮助我实现UICollectionViewController 的数据源方法的一些建议。
首先,我为分层的每种不同类型的视图使用了一个部分。这对我来说效果很好。我没有使用UICollectionView 的补充或装饰视图。我在视图控制器开头的枚举中为每个部分命名,如下所示:
enum STSectionNumbers
{
number_the_first_section_0_even_if_they_are_moved_during_editing = -1,
// Section names. Order implies z with earlier sections drawn behind latter sections.
STBackgroundCellsSection,
STDataCellSection,
STDayHeaderSection,
STColumnHeaderSection,
// The number of sections.
STSectionCount,
};
在我的UICollectionViewLayout 子类中,当要求布局属性时,我设置了 z 属性以满足如下顺序:
-(UICollectionViewLayoutAttributes*) layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
const CGRect frame = …
[attributes setFrame: frame];
[attributes setZIndex: [indexPath section]];
return attributes;
}
对我来说,如果我将 both 的 UICollectionView 实例 all 部分都提供给我,那么数据源逻辑会简单得多,但是通过将部分留空来控制哪个视图真正获取它们。
这是一个方便的方法,我可以用它来检查给定的UICollectionView真的是否有特定的节号:
-(BOOL) collectionView:(UICollectionView *const)collectionView hasSection:(const NSUInteger)section
{
const BOOL isForegroundView = collectionView == [self collectionView];
const BOOL isBackgroundView = !isForegroundView;
switch (section)
{
case STBackgroundCellsSection:
case STDataCellSection:
{
return isBackgroundView;
}
case STColumnHeaderSection:
case STDayHeaderSection:
{
return isForegroundView;
}
default:
{
return NO;
}
}
}
有了这个,编写数据源方法真的很容易。正如我所说,两个视图的节数相同,所以:
-(NSInteger) numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return STSectionCount;
}
但是,它们在部分中的单元格计数不同,但这很容易适应
-(NSInteger) collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
if(![self collectionView: collectionView hasSection: section])
{
return 0;
}
switch(section)
{
case STDataCellSection:
{
return … // (actual logic not shown)
}
case STBackgroundCellsSection:
{
return …
}
… // similarly for other sections.
default:
{
return 0;
}
}
}
我的UICollectionViewLayout 子类也有一些依赖于视图的方法,它委托给 UICollectionViewController 子类,但使用上面的模式可以轻松处理这些方法:
-(NSArray*) collectionViewRowRanges:(UICollectionView *)collectionView inSection:(NSInteger)section
{
if(![self collectionView: collectionView hasSection: section])
{
return [NSArray array];
}
switch(section)
{
case STDataCellSection:
{
return … // (actual logic omitted)
}
}
case STBackgroundCellsSection:
{
return …
}
… // etc for other sections
default:
{
return [NSArray array];
}
}
}
作为健全性检查,我确保集合视图只要求它们应该显示的部分中的单元格:
-(UICollectionViewCell*) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
assert([self collectionView: collectionView hasSection: [indexPath section]] && "Check views are only asking for the sections they own.");
switch([indexPath section])
{
case STBackgroundCellsSection:
… // You get the idea.
最后,值得注意的是,如another SA answer 所示,粘性部分的数学比我想象的要简单,前提是您将所有内容(包括设备的屏幕)都视为在集合视图的内容空间中.
UICollectionReusableView 重用类别的代码
@interface UICollectionReusableView (Reuse)
+(void) registerForReuseInView: (UICollectionView*) view;
+(id) dequeueFromView: (UICollectionView*) view withIndexPath: (NSIndexPath *) indexPath;
@end
它的实现是:
@implementation UICollectionReusableView (Reuse)
+(void) registerForReuseInView: (UICollectionView*) view
{
[view registerClass: self forCellWithReuseIdentifier: NSStringFromClass(self)];
}
+(instancetype) dequeueFromView: (UICollectionView*) view withIndexPath: (NSIndexPath *) indexPath
{
return [view dequeueReusableCellWithReuseIdentifier:NSStringFromClass(self) forIndexPath: indexPath];
}
@end