web-dev-qa-db-ja.com

UICollectionViewLayoutの無効化コンテキストの使用

そのため、shouldInvalidateLayoutForBoundsChange:からYESを返すことにより、UICollectionViewに機能するスティッキーヘッダーを部分的に実装しました。ただし、これはパフォーマンスに影響するため、レイアウト全体を無効にしたくありません。ヘッダーセクションだけを無効にします。

公式ドキュメントによると、私はUICollectionViewLayoutInvalidationContextを使用してレイアウトのカスタム無効化コンテキストを定義できますが、ドキュメントは非常に不足しています。 「個別に再計算できるレイアウトデータの部分を表すカスタムプロパティを定義する」ように要求されますが、これが何を意味するのか理解できません。

誰かがUICollectionViewLayoutInvalidationContextをサブクラス化した経験がありますか?

29
mattsson

これはiOS8用です

私は少し実験して、少なくともAppleがドキュメントを少し拡張するまで)、無効化レイアウトを使用するきれいな方法を見つけたと思います。

私が解決しようとしていた問題は、コレクションビューで粘着性のあるヘッダーを取得することでした。 FlowLayoutのサブクラスを使用し、layoutAttributesForElementsInRect:をオーバーライドするための作業コードがありました(Googleで作業例を見つけることができます)。これにより、shouldInvalidateLayoutForBoundsChange:から常にtrueを返す必要がありました。これは、Appleがコンテキストの無効化によって回避することを望んでいる、ナットの主要なパフォーマンスキックです。

クリーンコンテキストの無効化

UICollectionViewFlowLayoutをサブクラス化するだけで済みます。 UICollectionViewLayoutInvalidationContextのサブクラスは必要ありませんでしたが、これは非常に単純なユースケースになる可能性があります。

コレクションビューがスクロールすると、フローレイアウトはshouldInvalidateLayoutForBoundsChange:呼び出しの受信を開始します。フローレイアウトはすでにこれを処理できるため、関数の最後にスーパークラスの回答を返します。単純なスクロールでは、これはfalseになり、要素を再レイアウトしません。ただし、ヘッダーを再レイアウトして画面の上部に表示する必要があるため、提供するコンテキストのみを無効にするようにコレクションビューに指示します。

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
}

それでおしまい。ヘッダーだけが無効になります。フローレイアウトは、無効化コンテキストのindexPathを使用して、layoutAttributesForSupplementaryViewOfKind:への呼び出しを1つだけ受け取ります。セルまたはデコレーターを無効にする必要がある場合は、UICollectionViewLayoutInvalidationContextに他のinvalidate *関数があります。

最も難しい部分は、実際にはinvalidationContextForBoundsChange:関数のヘッダーのindexPathsを決定することです。ヘッダーとセルの両方のサイズは動的であり、境界CGRectを見ただけで機能するようにするには、いくつかのアクロバットが必要です。最も明確に役立つ関数であるindexPathForItemAtPoint:は、ポイントがヘッダー、フッター、デコレーター、または行間隔。

パフォーマンスに関しては、完全な測定は行いませんでしたが、スクロール中にTime Profilerをざっと見ると、正常に動作していることがわかります(右側の小さなスパイクはスクロール中です)。 UICollectionViewLayoutInvalidationContext performance comparison

32
meelawsh

私は今日、これについて同じ質問をしているだけでなく、その部分と混同しました:「個別に再計算できるレイアウトデータの部分を表すカスタムプロパティを定義する」

私がしたことは、サブクラスUICollectionViewLayoutInvalidationContextCustomInvalidationContextとしましょう)で、独自のプロパティを追加しました。テストの目的で、これらのプロパティを構成してコンテキストから取得できる場所を見つけたかったので、「属性」と呼ばれるプロパティとして配列を追加しました。

次に、サブクラス化されたUICollectionViewLayout+invalidationContextClassを上書きして、CustomInvalidationContextのインスタンスを返します。これは、別のメソッド-invalidationContextForBoundsChangeで上書きした別のメソッドで返されます。このメソッドでは、Superを呼び出してCustomInvalidationContextのインスタンスを返し、それからプロパティを構成して返す必要があります。属性配列を設定してオブジェクトを設定します@["a","b","c"];

これは、後でさらに別の上書きされたメソッド-invalidateLayoutWithContext:で取得されます。渡されたコンテキストから設定した属性を取得できました。

したがって、後は、-layoutAttributesForElementsInRect:に提供するindexPathを計算できるプロパティを設定することができます。

それが役に立てば幸い。

7
Peeks

IOS 8現在

この回答はiOS 8シードの前に書かれました。 iOS 7には存在しなかった機能に期待されていることについて言及し、回避策を提供しています。この機能は現在存在し、機能しています。別の答えは、現在ページのさらに下にあり、 iOS 8へのアプローチ について説明しています。


討論

まず、あらゆる種類の最適化において、本当に重要な注意は、最初にプロファイルを作成し、パフォーマンスのボトルネックがどこにあるかを正確に理解することです。

UICollectionViewLayoutInvalidationContextを確認しましたが、必要な機能が提供されているようです。質問へのコメントで、これを機能させるための私の試みについて説明しました。レイアウトの再計算を削除することはできますが、コンテンツセルのレイアウトを変更しないようにするのには役立ちません。私の場合、レイアウトの計算はそれほど高価ではありませんが、単純なスクロールセル(かなりの数があります)にレイアウトの変更を適用するフレームワークを避け、それらを「特別な」セルにのみ適用します。

実装の概要

Appleが意図したようにそれを実行できなかったことを考慮して、私はだまされました。 2UICollectionViewインスタンスを使用しています。バックグラウンドビューには通常のスクロールコンテンツがあり、2番目のフォアグラウンドビューにはヘッダーがあります。ビューのレイアウトは、境界の変更時に背景ビューが無効にならないように指定し、前景ビューは無効にします。

実装の詳細

この作業を正しく行うために必要な明白でないことがいくつかあります。また、実装を簡単にするためのヒントもいくつかあります。これについて説明し、アプリケーションから取得したコードの一部を提供します。ここでは完全なソリューションを提供するつもりはありませんが、必要なすべての要素を提供します。

UICollectionViewにはbackgroundViewプロパティがあります。

UICollectionViewControllerviewDidLoadメソッドで背景ビューを作成します。この時点で、ビューコントローラーのUICollectionViewプロパティにcollectionViewインスタンスが既に存在します。これはフォアグラウンドビューになり、ピン留めなどの特別なスクロール動作を持つアイテムに使用されます。

2番目の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];
…
_

要約すると、この時点で2つのコレクションビューが重なり合っています。背面の1つは静的コンテンツに使用されます。フロントは固定されたコンテンツなどのためのものです。どちらも、デリゲートおよびデータソースと同じUICollectionViewControllerを指しています。

STGridLayoutのinvalidateLayoutForBoundsChangeプロパティは、カスタムレイアウトに追加したものです。レイアウトは、-(BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBoundsが呼び出されたときにそれを返すだけです。

次に、両方のビューに共通のセットアップがあり、私の場合は次のようになります。

_for(UICollectionView *collectionView in @[foregroundCollectionView, backgroundCollectionView])
{
  // Configure reusable views.
  [STCollectionViewStaticCVCell registerForReuseInView: collectionView];
  [STBlockRenderedCollectionViewCell registerForReuseInView: collectionView];
}
_

_registerForReuseInView:_メソッドは、_dequeueFromView:_とともに、カテゴリによってUICollectionReusableViewに追加されたものです。これらのコードは回答の最後にあります。

viewDidLoadの次の部分は、このアプローチで唯一の大きな頭痛の種です。

フォアグラウンドビューをドラッグするときは、バックグラウンドビューでスクロールする必要があります。このコードをすぐに示します。これは、フォアグラウンドビューのcontentOffsetをバックグラウンドビューにミラーリングするだけです。ただし、コンテンツの端でスクロールビューが「バウンス」できるようにする必要があります。コンテンツがUICollectionViewの境界から切り離されないようにプログラムで設定されている場合、contentOffsetclampUICollectionViewになるようです。救済策がないと、フォアグラウンドの粘着性のある要素だけが跳ね返り、恐ろしく見えます。ただし、以下を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;
}
_

私の場合、データソースロジックはmuchを指定すると、より単純になりますboth of the UICollectionView instances allセクションを指定しますが、実際に取得するビューを制御しますもう一方のセクションを空にすることでそれらを作成します。

以下は、特定のUICollectionViewreallyに特定のセクション番号があるかどうかを確認するために使用できる便利なメソッドです。

_-(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
_
4
Benjohn

PrepareLayoutが呼び出された理由を示すフラグを設定し、粘着性のあるセルの位置のみを再計算することで、実行しようとしていることを正確に実装しました。

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

それからprepareLayoutで私はします:

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

私はカスタムUICollectionViewLayoutサブクラスに取り組んでいます。 UICollectionViewLayoutInvalidationContextを使ってみました。すべての属性を再計算するためにUICollectionViewLayout全体を必要としないレイアウト/ビューを更新すると、invalidateLayoutWithContext:UICollectionViewLayoutInvalidationContextサブクラスを使用し、prepareLayoutで属性のみを再計算しますこれは、すべての属性を再計算するのではなく、my UICollectionViewLayoutInvalidationContextサブクラスプロパティで指定されます。