为UICollectionViewLayout使用失效上下文

所以我已经在我的UICollectionView中实现了工作粘滞标题,部分原因YESshouldInvalidateLayoutForBoundsChange:返回YES 。 但是,这会影响性能,我不想使整个布局无效,只有我的标题部分。

现在,根据官方文档,我可以使用UICollectionViewLayoutInvalidationContext为我的布局定义一个自定义的无效上下文,但文档是非常缺乏。 它要求我“定义代表可以独立重新计算的布局数据部分的自定义属性”,但我不明白这是什么意思。

有没有人有任何经验UICollectionViewLayoutInvalidationContext

这是iOS8

我尝试了一下,我想我已经find了使用无效布局的简单方法,至less在苹果公司对文档做了一些扩展之前。

我试图解决的问题是在收集视图中获取粘性标题。 我有使用FlowLayout的子类的工作代码,并重写layoutAttributesForElementsInRect :(你可以find谷歌工作的例子)。 这要求我始终从shouldInvalidateLayoutForBoundsChange返回true:这是苹果希望我们通过上下文无效化来避免的坚果中的主要performance。

干净的上下文无效

你只需要inheritanceUICollectionViewFlowLayout。 我不需要UICollectionViewLayoutInvalidationContext的子类,但这可能是一个非常简单的用例。

当集合视图滚动时,stream布局将开始接收shouldInvalidateLayoutForBoundsChange:calls。 由于stream布局已经可以处理这个,所以我们将在函数结尾处返回超类的答案。 用简单的滚动这将是错误的,不会重新布局的元素。 但是我们需要重新排列标题,让它们停留在屏幕的顶部,所以我们要告诉集合视图,只让我们提供的上下文无效:

 override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { invalidateLayoutWithContext(invalidationContextForBoundsChange(newBounds)) return super.shouldInvalidateLayoutForBoundsChange(newBounds) } 

这意味着我们也需要重写invalidationContextForBoundsChange:函数。 由于这个函数的内部工作是未知的,我们只要求超类的无效上下文对象,确定哪些集合视图元素我们想要无效,并将这些元素添加到无效上下文。 我拿出了一些代码,专注于这里的要点:

 override func invalidationContextForBoundsChange(newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext! { var context = super.invalidationContextForBoundsChange(newBounds) if /... we find a header in newBounds that needs to be invalidated .../ { context.invalidateSupplementaryElementsOfKind(UICollectionElementKindSectionHeader, atIndexPaths:[NSIndexPath(forItem: 0, inSection:headerIndexPath.section)] ) } return context } 

而已。 标题和标题都是无效的。 stream布局将只接收一次对layoutAttributesForSupplementaryViewOfKind的调用:在失效上下文中使用indexPath。 如果您需要使单元或装饰器无效,则在UICollectionViewLayoutInvalidationContext上还有其他invalidate *函数。

最难的部分确实是在invalidationContextForBoundsChange:函数中确定标头的indexPaths。 我的头文件和单元格都是dynamicresize的,并且只需要查看边界CGRect,就可以使用一些杂技来工作,因为最明显有用的函数indexPathForItemAtPoint:如果点位于页眉,页脚,装饰器或行间距。

至于性能方面,我并没有进行全面的测量,而是快速浏览一下Time Profiler,而滚动显示它正在做一些正确的事情(滚动时右侧较小的秒数)。 UICollectionViewLayoutInvalidationContext性能比较

今天我只是提出了同样的问题,也对这个部分感到困惑:“定义代表可以独立重新计算的布局数据部分的自定义属性”

我所做的是UICollectionViewLayoutInvalidationContext子类(我们称之为CustomInvalidationContext )并添加了我自己的属性。 为了testing目的,我想找出我可以在上下文中configuration和检索这些属性的位置,所以我简单地将一个数组添加为一个名为“attributes”的属性。

然后在我的子类UICollectionViewLayout覆盖+invalidationContextClass返回一个实例的CustomInvalidationContext在另一个方法中返回我覆盖是: -invalidationContextForBoundsChange 。 在这个方法中,你需要调用super来返回CustomInvalidationContext的实例,然后你可以configuration属性并返回。 我将属性数组设置为具有对象@["a","b","c"];

然后在另一个被覆盖的方法 – invalidateLayoutWithContext:检索。 我能够从传入的上下文中检索我设置的属性。

所以你可以做的是设置属性,稍后将允许你计算什么indexPaths提供给-layoutAttributesForElementsInRect:

希望能帮助到你。

截至iOS 8

这个答案是在iOS 8种子之前写的。 它提到了希望iOS 7中不存在的function,并提供解决方法。 这个function现在已经存在并且工作。 另一个回答,目前在页面的下方,描述了iOS 8的一种方法 。


讨论

首先,通过任何forms的优化,真正重要的一点是要首先进行简介,并了解性能瓶颈的确切位置。

我已经看了UICollectionViewLayoutInvalidationContext并同意它似乎可能提供所需的function。 在对这个问题的评论中,我描述了我试图做到这一点的工作。 我现在怀疑,虽然它允许您删除布局重新计算,但它不会帮助您避免对内容单元进行布局更改。 在我的情况下,布局计算不是特别昂贵,但我想避免框架应用布局更改简单的滚动单元格(我有很多),只适用于“特殊”单元格。

执行摘要

鉴于苹果公司似乎没有这样做,我就欺骗了。 我使用2个 UICollectionView实例。 我有一个背景视图上正常的滚动内容,并在第二个前景视图上的标题。 视图的布局指定背景视图不会使边界更改无效,而前景视图也不会。

实施细节

有很多非常明显的事情需要你做出正确的工作,而且我还得到了一些实现的技巧,这些技巧让我的工作更轻松。 我会通过这个,并提供从我的应用程序取得的代码剪辑。 我不打算提供一个完整的解决scheme,但我会给你所需要的所有部分。

UICollectionView有一个backgroundView属性。

我在我的UICollectionViewControllerviewDidLoad方法中创build了背景视图。 通过这一点,视图控制器已经在其collectionView属性中有一个UICollectionView实例。 这将成为前景视图,并将用于具有特殊滚动行为的项目,如钉住。

我创build了第二个UICollectionView实例,并将其设置为前景集合视图的backgroundView属性。 我设置的背景也使用UICollectionViewController子类作为它的数据源和委托。 我禁用用户交互的背景视图,因为它否则似乎获得所有事件。 如果你想要select等,你可能需要更微妙的行为:

 … 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将在编程设置时限制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]; } } 

实施提示

这些build议帮助我实现了UICollectionViewController的数据源方法。

首先,我已经使用了一个部分为每个不同types的视图分层。 这对我很好。 我没有使用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; } 

对于我来说,如果我将两个 UICollectionView实例 UICollectionView所有这些部分,那么数据源逻辑就简单了,但是通过使这些部分为另一个部分为空来控制哪个视图真正获得了它们。

这里有一个方便的方法,我可以用来检查一个给定的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. 

最后,值得注意的是,如另一个SA回答所示,粘性部分的math比我想象的要简单,因为您可以将所有内容(包括设备的屏幕)都视为在收集视图的内容空间中。

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 

我通过设置一个标志告诉我为什么prepareLayout被调用,然后只重新计算粘性单元的位置,我实现了你正在做的事情。

 - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { _invalidatedBecauseOfBoundsChange = YES; return YES; } 

然后在prepareLayout我做:

 if (!_invalidatedBecauseOfBoundsChange) { [self calculateStickyCellsPositions]; } _invalidateBecauseOfBoundsChange = NO; 

我正在处理自定义的UICollectionViewLayout子类。 我试图使用UICollectionViewLayoutInvalidationContext 。 当我更新不需要整个UICollectionViewLayout来重新计算其所有属性的布局/视图时,我在invalidateLayoutWithContext:使用我的UICollectionViewLayoutInvalidationContext子类invalidateLayoutWithContext:并在prepareLayout仅重新计算在我的UICollectionViewLayoutInvalidationContext子类属性中指定的属性,而不是重新计算所有属性。