UICollectionViewLayoutの実装方法

UICollectionViewLayoutの実装方法iOS6で追加されたUICollectionView、色々と夢が広がる感じです。

https://twitter.com/olebegemann/status/248380531690045440

(数ヶ月後には、UICollectionViewLayoutのサブクラスがGitHubにあふれているだろう!)

#まだほとんど無いようですが。。。

色々とカスタマイズしようと思うと、まだ情報が少なく、よくわからない部分が多いです。

DataSourceまわりの扱い(UITableViewでおなじみな感じ)、UICollectionViewFlowLayoutを使った基本的な使い方については、こちらが参考になります。
Natsu's Note
 [iOS6] Collection View 基本的な使い方
今回は、カスタムなレイアウトを実現するために、UICollectionViewLayout(UICollectionViewFlowLayoutではない)をサブクラス化して使う場合について、WWDC2012のサンプルコード(CircleLayout)をもとに解説してみます。

ただし、Appleが配布しているWWDC2012のサンプルコードは、iOS6ベータ時代のもののようで、そのままではまともに動きません。期待通りに動くように修正されたコードが以下にあります。)

元ネタは以下。

http://markpospesel.wordpress.com/2012/10/25/fixing-circlelayout/

コードはこちら。

https://github.com/mpospese/CircleLayout

 

Collection View で利用するクラス

サンプルコードを見るまえに、ざっとCollectionViewについて触れておきます。

Collection View を使う際の主なクラスは、
  • UICollectionView
  • UICollectionViewController
  • UICollectionViewCell
といったあたりですが、UITableViewを扱ったことがある方なら、名前から大体使い方はイメージできるでしょう。Collection View では、そのレイアウトの自由度を高めるために専用のクラスが用意されており、ViewControllerと合わせて使うこととなります。
  • UICollectionViewFlowLayout
  • UICollectionViewLayout
UICollectionViewFlowLayoutは、UICollectionViewLayoutのサブクラスであり、UICollectionViewLayoutは抽象クラスです。UICollectionViewFlowLayoutでは表現できないようはカスタムレイアウトを行ないたい場合は、UICollectionViewLayoutをサブクラス化してレイアウト制御を実装することとなります。このレイアウトクラスが、UITableViewとの最大の差異となっている箇所です。

Collection Viewは、割り当てたLayoutオブジェクトにレイアウト情報を問い合わせ、Layoutオブジェクトがレイアウト情報を必要に応じて返答することで、レイアウトが制御されます。

そのほかに、
  • UICollectionViewLayoutAttributes
  • UICollectionViewUpdateItem
あたりのクラスも押さえておく必要があるかと思います。UICollectionViewLayoutAttributesクラスは、Collection View上の各Cellのレイアウト情報(大きさ、座標、α値などの属性情報)をCellに紐付けて取り扱いやすいようにまとめたクラスです。
UICollectionViewUpdateItemについては、後ほど説明します。

 

UICollectionViewLayout のサブクラス化

サンプルコードを見る前の前準備としてもう一つ、UICollectionViewLayoutで使われるメソッド群について触れておきます。

UICollectionViewLayoutは抽象クラスのため、カスタムなレイアウトを実装する際には、このクラスをサブクラス化し、必要なメソッドを実装していくこととなります。

Appleのドキュメントは、オーバーライドすべき(should)メソッドとして以下が記載されています。

 

collectionViewContentSize

Collection View のコンテンツのサイズを返す。

 

layoutAttributesForElementsInRect:

Rectで指定されたエリアに存在するCell群のレイアウト情報(UICollectionViewLayoutAttributesクラスのオブジェクト)をNSArrayで返す。

 

layoutAttributesForItemAtIndexPath:

NSIndexPathで指定されたCellのレイアウト情報(UICollectionViewLayoutAttributesクラスのオブジェクト)を返す

 

layoutAttributesForSupplementaryViewOfKind:atIndexPath:

supplementary view(今回は説明省略)を利用する場合にそのレイアウト情報を生成する。

 

layoutAttributesForDecorationViewOfKind:atIndexPath:

decoration view(今回は説明省略)を利用する場合にそのレイアウト情報を生成する。

 

shouldInvalidateLayoutForBoundsChange:

Collection Viewのboundsが変更された際に、レイアウトし直すかをBOOLで返す。

 

上記がオーバーライドすべき(should)メソッドとして指定されていますが、上記以外にも、よく使いそうなメソッドとして
  • prepareLayout
  • prepareForCollectionViewUpdates:
  • finalizeCollectionViewUpdates
  • initialLayoutAttributesForAppearingItemAtIndexPath:
  • finalLayoutAttributesForDisappearingItemAtIndexPath:
  • invalidateLayout
といったものがあります。

 

レイアウト情報生成に呼び出されるメソッドの流れ

上記を踏まえ、サンプルコードのCircleLayoutプロジェクトを追いかけていきます。

CircleLayoutプロジェクトは、丸いCellを円状にレイアウトし、かつ、Cellをタップした際にはアニメーションしながらCellを削除、背景をタップした際にはアニメーションしながらCellを挿入するデモコードです。

サンプルコードでは、CircleLayout.mがUICollectionViewLayoutのサブクラス実装です。今回の説明はこの部分にフォーカスして説明していきます。

このコードを見ながら、先に紹介したLayoutクラスの各メソッドが、UICollectionViewがレイアウト情報を取得する際に、主要なメソッド群がどのように呼び出されるかの流れを、呼び出される順に見て行きます。

 

 

1. prepareLayout

レイアウトの計算に必要となるような、前処理を行う。

-(void)prepareLayout
{
    [super prepareLayout];

    CGSize size = self.collectionView.frame.size;
    _cellCount = [[self collectionView] numberOfItemsInSection:0];
    _center = CGPointMake(size.width / 2.0, size.height / 2.0);
    _radius = MIN(size.width, size.height) / 2.5;
}

ここで何をすべきというお約束は特にないので、実装するレイアウト計算に必要な前処理を行なう部分となります。

 

 

2. collectionViewContentSize

コンテンツサイズを返します。

-(CGSize)collectionViewContentSize
{
    return [self collectionView].frame.size;
}

まあ、そのままですね。

 

 

3. prepareForCollectionViewUpdates

これは、Cellの削除、追加、移動処理が行なわれた際にのみ呼ばれます。
削除、追加、移動処理のための前処理を定義します。

- (void)prepareForCollectionViewUpdates:(NSArray *)updateItems
{
    // Keep track of insert and delete index paths
    [super prepareForCollectionViewUpdates:updateItems];

    self.deleteIndexPaths = [NSMutableArray array];
    self.insertIndexPaths = [NSMutableArray array];

    for (UICollectionViewUpdateItem *update in updateItems)
    {
        if (update.updateAction == UICollectionUpdateActionDelete)
        {
            [self.deleteIndexPaths addObject:update.indexPathBeforeUpdate];
        }
        else if (update.updateAction == UICollectionUpdateActionInsert)
        {
            [self.insertIndexPaths addObject:update.indexPathAfterUpdate];
        }
    }
}

このメソッドでは、お約束の実装パターンが必要となります。

渡されるupdateItemsのArrayは、UICollectionViewUpdateItemオブジェクトからなるArrayで、UICollectionViewUpdateItemオブジェクトを利用し、削除、挿入、移動などの更新処理がなされたCellを特定することができます。
Cellの削除、挿入、移動のアニメーション処理のために、更新処理が行なわれた対象Cellの情報(indexPath)をここで保持しておく処理を行なっています。

また、

    [super prepareForCollectionViewUpdates:updateItems];

の行は、お約束として必ず入れておいてください。この行がないと、アニメーションされません。

 

 

4. layoutAttributesForElementsInRect + layoutAttributesForItemAtIndexPath

rectで指定された領域に存在するCell群のUICollectionViewLayoutAttributesオブジェクト(以降、attributesと略記)をArrayで返す実装をここでします。

-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray* attributes = [NSMutableArray array];
    for (NSInteger i=0 ; i < self.cellCount; i++) {
        NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        [attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
    }
    return attributes;
}

CircleLayoutプロジェクトでは、全てのCellが表示領域に存在しているため、このメソッドの中で、存在する全てのCell数分のattributesを生成しています。attributes生成そのものは、layoutAttributesForItemAtIndexPathメソッドで行なう構造となっています。

この構造だけ見ると、layoutAttributesForItemAtIndexPath メソッド必要なの?と思いますが、後に出る、initialLayoutAttributesForAppearingItemAtIndexPath,finalLayoutAttributesForDisappearingItemAtIndexPathなど、Cell単体でのattributes取得が必要な場面が出てくるので、お約束として実装しておきます。

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
    UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
    attributes.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
    attributes.center = CGPointMake(_center.x + _radius * cosf(2 * path.item * M_PI / _cellCount),
                                    _center.y + _radius * sinf(2 * path.item * M_PI / _cellCount));
    return attributes;
}

cellのindexPath情報に紐付けてUICollectionViewLayoutAttributesオブジェクトを生成し、Cellのサイズや位置を定義しています。

 

 

5. initialLayoutAttributesForAppearingItemAtIndexPath:

Cellを挿入した際に、挿入アニメーションを実現するために存在するメソッドです。
挿入初期状態のCellのattributesを返すようにします。
ここで生成したattributesの状態から、最終的な状態(挿入完了状態)のattributes (layoutAttributesForItemAtIndexPathで生成されるもの)へとアニメーションされることとなります。

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    // Must call super
    UICollectionViewLayoutAttributes *attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];

    if ([self.insertIndexPaths containsObject:itemIndexPath])
    {
        // only change attributes on inserted cells
        if (!attributes)
            attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];

        // Configure attributes ...
        attributes.alpha = 0.0;
        attributes.center = CGPointMake(_center.x, _center.y);
    }

    return attributes;
}

このメソッドがくせ者で、実装すべき内容を誤解しやすいのですが、、、、
このメソッドは、挿入されたCell分だけでなく、もともと存在するCellを含め表示する全てのCell毎に呼び出されます。(もともと9個のCellがある状態から1個のCellを追加した場合には、このメソッドは10回呼ばれます。)

ついつい、挿入対象のCellの初期状態のattributesのことだけ考えてしまいそうになりますが、元々存在しているCellの挿入処理前のattributesも、それぞれのCellに対して応答する必要があるわけです。

以下の行で、全てのCellに対し、挿入前状態のattributesを取得しています。

    UICollectionViewLayoutAttributes *attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];

さらに、

    if ([self.insertIndexPaths containsObject:itemIndexPath])

の部分で、指定されたindexPathが新たに挿入されたCellか、元々存在しているCellかを判定しています。
ここで判定に利用している、self.insertIndexPath は、3. prepareForCollectionViewUpdates で用意したものを利用しています。

 

 

6. finalLayoutAttributesForDisappearingItemAtIndexPath:

Cellを削除した際に、削除アニメーションを実現するために存在するメソッドです。
削除完了状態のCellのattributesを返すようにします。

- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    // So far, calling super hasn't been strictly necessary here, but leaving it in
    // for good measure
    UICollectionViewLayoutAttributes *attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath];

    if ([self.deleteIndexPaths containsObject:itemIndexPath])
    {
        // only change attributes on deleted cells
       if (!attributes)
            attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];

        // Configure attributes ...
        attributes.alpha = 0.0;
        attributes.center = CGPointMake(_center.x, _center.y);
        attributes.transform3D = CATransform3DMakeScale(0.1, 0.1, 1.0);
    }

    return attributes;
}

実装の考え方は、5.の挿入と同様なので、説明は省略。

 

 

7. finalizeCollectionViewUpdates

一連のレイアウト情報生成が完了後、最後に呼ばれるメソッドです。

- (void)finalizeCollectionViewUpdates
{
    [super finalizeCollectionViewUpdates];
    // release the insert and delete index paths
    self.deleteIndexPaths = nil;
    self.insertIndexPaths = nil;
}

レイアウト情報生成に利用したデータをクリアしています。

長くなりましたが、以上です。

もし、記載内容に誤り等あれば、ご指摘頂けますと幸いです。


Comments are closed.