web-dev-qa-db-ja.com

自動レイアウトを使用してUICollectionViewでセルの1次元を指定する

IOS 8では、UICollectionViewFlowLayoutは、コンテンツサイズに基づいてセルのサイズを自動的に変更することをサポートします。これにより、コンテンツに応じて幅と高さの両方でセルのサイズが変更されます。

すべてのセルの幅(または高さ)に固定値を指定し、他の寸法のサイズを変更することは可能ですか?

簡単な例として、セル内の複数行ラベルを考えてみてください。セルの横に配置する制約があります。複数行のラベルは、テキストに合わせてさまざまな方法でサイズを変更できます。セルはコレクションビューの幅を埋め、それに応じて高さを調整する必要があります。代わりに、セルは無計画にサイズ調整され、セルサイズがコレクションビューのスクロール不可能なディメンションよりも大きい場合にクラッシュを引き起こします。


iOS 8では、メソッドsystemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority:が導入されています。コレクションビューの各セルに対して、レイアウトはセルでこのメソッドを呼び出し、推定サイズを渡します。私にとって意味があるのは、セルでこのメソッドをオーバーライドし、指定されたサイズを渡して、水平方向の制約をrequiredに設定し、垂直方向の制約に低い優先度を設定することです。これにより、水平サイズはレイアウトで設定された値に固定され、垂直サイズは柔軟になります。

このようなもの:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
    attributes.size = [self systemLayoutSizeFittingSize:layoutAttributes.size withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    return attributes;
}

ただし、この方法で返されるサイズは完全に奇妙です。この方法のドキュメントは私には非常に不明確であり、ゼロサイズとかなり大きなサイズを表す定数UILayoutFittingCompressedSizeUILayoutFittingExpandedSizeを使用することに言及しています。

このメソッドのsizeパラメーターは、実際には2つの定数を渡すだけの方法ですか?特定のサイズに適切な高さを取得することを期待する動作を達成する方法はありませんか?


代替ソリューション

1)セルに特定の幅を指定する制約を追加すると、正しいレイアウトが実現します。この制約は、安全な参照を持たないセルのコレクションビューのサイズに設定する必要があるため、貧弱なソリューションです。セルの構成時にその制約の値を渡すこともできますが、それはまったく直観に反しているようにも見えます。また、セルまたはそのコンテンツビューに制約を直接追加すると多くの問題が発生するため、これも厄介です。

2)テーブルビューを使用します。セルの幅が固定されているため、テーブルビューはそのまま使用できますが、複数列に固定幅のセルがあるiPadレイアウトのような他の状況には対応できません。

108
Anthony Mattox

あなたが求めているのは、UICollectionViewを使用してUITableViewのようなレイアウトを作成する方法であるように思えます。それが本当にあなたが望むものである場合、これを行う正しい方法は、カスタムUICollectionViewLayoutサブクラスを使用することです(おそらく SBTableLayout のようなものです)。

一方、デフォルトのUICollectionViewFlowLayoutを使用してこれを行うためのクリーンな方法があるかどうかを本当に質問している場合、方法はないと考えています。 iOS8の自己サイズ変更セルを使用しても、簡単ではありません。あなたが言うように、基本的な問題は、フローレイアウトの機構が1つの次元を修正し、別の次元に応答させる方法を提供しないことです。 (さらに、できたとしても、複数行のラベルのサイズを設定するために2つのレイアウトパスが必要になると、さらに複雑になります。

ただし、独自のサイズを決定し、コレクションビューの幅に自然に応答するセルを使用して、フローレイアウトでTableviewのようなレイアウトを作成する場合は、もちろん可能です。まだ厄介な方法があります。 「サイズ変更セル」、つまりコントローラーがセルサイズの計算のためだけに保持する非表示のUICollectionViewCellを使用してそれを実行しました。

このアプローチには2つの部分があります。最初の部分は、コレクションビューの幅を取得し、サイズ変更セルを使用してセルの高さを計算することにより、コレクションビューのデリゲートが正しいセルサイズを計算することです。

UICollectionViewDelegateFlowLayoutで、次のようなメソッドを実装します。

func collectionView(collectionView: UICollectionView,
  layout collectionViewLayout: UICollectionViewLayout,
  sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
  // NOTE: here is where we say we want cells to use the width of the collection view
   let requiredWidth = collectionView.bounds.size.width

   // NOTE: here is where we ask our sizing cell to compute what height it needs
  let targetSize = CGSize(width: requiredWidth, height: 0)
  /// NOTE: populate the sizing cell's contents so it can compute accurately
  self.sizingCell.label.text = items[indexPath.row]
  let adequateSize = self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
  return adequateSize
}

これにより、コレクションビューは、囲むコレクションビューに基づいてセルの幅を設定しますが、サイズ変更セルに高さの計算を要求します。

2番目の部分は、サイジングセルに独自のAL制約を使用して高さを計算させることです。これは、複数行のUILabelが2段階のレイアウトプロセスを効率的に必要とする方法のため、本来よりも難しい場合があります。作業はメソッドpreferredLayoutSizeFittingSizeで行われ、次のようになります。

 /*
 Computes the size the cell will need to be to fit within targetSize.

 targetSize should be used to pass in a width.

 the returned size will have the same width, and the height which is
 calculated by Auto Layout so that the contents of the cell (i.e., text in the label)
 can fit within that width.

 */
 func preferredLayoutSizeFittingSize(targetSize:CGSize) -> CGSize {

   // save original frame and preferredMaxLayoutWidth
   let originalFrame = self.frame
   let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth

   // assert: targetSize.width has the required width of the cell

   // step1: set the cell.frame to use that width
   var frame = self.frame
   frame.size = targetSize
   self.frame = frame

   // step2: layout the cell
   self.setNeedsLayout()
   self.layoutIfNeeded()
   self.label.preferredMaxLayoutWidth = self.label.bounds.size.width

   // assert: the label's bounds and preferredMaxLayoutWidth are set to the width required by the cell's width

   // step3: compute how tall the cell needs to be

   // this causes the cell to compute the height it needs, which it does by asking the 
   // label what height it needs to wrap within its current bounds (which we just set).
   let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

   // assert: computedSize has the needed height for the cell

   // Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes."
   let newSize = CGSize(width:targetSize.width,height:computedSize.height)

   // restore old frame and preferredMaxLayoutWidth
   self.frame = originalFrame
   self.label.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth

   return newSize
 }

(このコードは、「高度なコレクションビュー」でのWWDC2014セッションのサンプルコードのAppleサンプルコードから採用されています。)

いくつかの注意点があります。ラベルの幅を計算して設定するために、layoutIfNeeded()を使用してセル全体のレイアウトを強制します。しかし、それだけでは十分ではありません。ラベルが自動レイアウトでその幅を使用するように、preferredMaxLayoutWidthも設定する必要があると思います。そして、ラベルを考慮に入れてセルに高さを計算させるためにsystemLayoutSizeFittingSizeを使用することができます。

私はこのアプローチが好きですか?番号!!複雑すぎると感じ、レイアウトを2回行います。ただし、パフォーマンスが問題にならない限り、実行時にレイアウトを2回実行するのではなく、コードで2回定義する必要があります。

私の希望は、最終的には自己サイズ調整セルの動作が異なり、これがすべて非常に簡単になることです。

サンプルプロジェクト 作業中に表示しています。

しかし、なぜ自己サイズのセルを使用しないのですか?

理論的には、iOS8の「セルの自己サイズ調整」のための新しい機能により、これは不要になります。自動レイアウト(AL)を使用してセルを定義した場合、コレクションビューは、それ自体のサイズを調整し、正しくレイアウトできるように十分スマートでなければなりません。実際には、これが複数行ラベルで機能するようになった例を見たことはありません。 this は、自己サイズ変更セルのメカニズムがまだバグがあるためだと思います。

しかし、それは主に自動レイアウトとラベルの通常のトリッキーさが原因であると思われます。つまり、UILabelsには基本的に2段階のレイアウトプロセスが必要です。自己サイズのセルを使用して両方のステップを実行する方法は明確ではありません。

そして私が言ったように、これは本当に異なるレイアウトの仕事です。幅を固定して高さを選択できるようにするのではなく、サイズを持つものを配置することがフローレイアウトの本質の一部です。

また、preferredLayoutAttributesFittingAttributesについてはどうでしょうか?

preferredLayoutAttributesFittingAttributes:メソッドはニシンです。これは、新しい自己サイズ調整セルメカニズムで使用するためだけのものです。したがって、そのメカニズムが信頼できない限り、これは答えではありません。

そして、systemlayoutSizeFittingSize:の最新情報

あなたはドキュメントが混乱しているのは正しいです。

systemLayoutSizeFittingSize:およびsystemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:の両方のドキュメントは、UILayoutFittingCompressedSizeとしてUILayoutFittingExpandedSizeおよびtargetSizeのみを渡すことを推奨しています。ただし、メソッドシグネチャ自体、ヘッダーコメント、および関数の動作は、targetSizeパラメーターの正確な値に応答していることを示しています。

実際、UICollectionViewFlowLayoutDelegate.estimatedItemSizeを設定すると、新しい自己サイズ調整セルメカニズムを有効にするために、その値がtargetSizeとして渡されるようです。そして、UILabel.systemLayoutSizeFittingSizeUILabel.sizeThatFitsとまったく同じ値を返すようです。 systemLayoutSizeFittingSizeへの引数は大まかなターゲットであり、sizeThatFits:への引数は最大外接サイズであると想定されるため、これは疑わしいです。

その他のリソース

このような日常的な要件に「研究リソース」が必要だと考えるのは悲しいことですが、必要だと思います。良い例と議論は次のとおりです。

132
algal

これを行うには、他のいくつかの回答よりもクリーンな方法があり、うまく機能します。パフォーマンスが高く(コレクションビューの読み込みが高速で、不要な自動レイアウトパスなどがない)、コレクションビューの幅が固定されているような「マジックナンバー」はありません。コレクションビューのサイズを変更します。回転時に、レイアウトを無効にすることもうまくいくはずです。

1.次のフローレイアウトサブクラスを作成します

class HorizontallyFlushCollectionViewFlowLayout: UICollectionViewFlowLayout {

    // Don't forget to use this class in your storyboard (or code, .xib etc)

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes
        guard let collectionView = collectionView else { return attributes }
        attributes?.bounds.size.width = collectionView.bounds.width - sectionInset.left - sectionInset.right
        return attributes
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let allAttributes = super.layoutAttributesForElementsInRect(rect)
        return allAttributes?.flatMap { attributes in
            switch attributes.representedElementCategory {
            case .Cell: return layoutAttributesForItemAtIndexPath(attributes.indexPath)
            default: return attributes
            }
        }
    }
}

2.自動サイズ設定のためにコレクションビューを登録します

// The provided size should be a plausible estimate of the actual
// size. You can set your item size in your storyboard
// to a good estimate and use the code below. Otherwise,
// you can provide it manually too, e.g. CGSize(width: 100, height: 100)

flowLayout.estimatedItemSize = flowLayout.itemSize

3.セルサブクラスで定義済みの幅+カスタムの高さを使用する

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutAttributes.bounds.size.height = systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    return layoutAttributes
}
49
Jordan Smith

IOS 9で数行のコードでそれを行う簡単な方法-水平方向の例(その高さをコレクションビューの高さに固定する):

自己サイズ変更セルを有効にするには、estimatedItemSizeを使用してコレクションビューフローレイアウトを初期化します。

self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.estimatedItemSize = CGSizeMake(1, 1);

コレクションビューレイアウトデリゲート(ほとんどの場合、View Controllerで)collectionView:layout:sizeForItemAtIndexPath:を実装します。ここでの目標は、コレクションビューディメンションに固定の高さ(または幅)を設定することです。 10の値は何でも構いませんが、制約を壊さない値に設定する必要があります。

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return CGSizeMake(10, CGRectGetHeight(collectionView.bounds));
}

カスタムセルpreferredLayoutAttributesFittingAttributes:メソッドをオーバーライドします。この部分では、自動レイアウトの制約と設定した高さに基づいて動的なセル幅を実際に計算します。

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    UICollectionViewLayoutAttributes *attributes = [layoutAttributes copy];
    float desiredWidth = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].width;
    CGRect frame = attributes.frame;
    frame.size.width = desiredWidth;
    attributes.frame = frame;
    return attributes;
}
6
Tanguy G.

優先レイアウト属性で幅を修正してみてください。

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [[super preferredLayoutAttributesFittingAttributes:layoutAttributes] copy];
    CGSize newSize = [self systemLayoutSizeFittingSize:CGSizeMake(FIXED_WIDTH,layoutAttributes.size) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGRect newFrame = attr.frame;
    newFrame.size.height = size.height;
    attr.frame = newFrame;
    return attr;
}

当然のことながら、レイアウトを次のように正しく設定する必要があります。

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout;
flowLayout.estimatedItemSize = CGSizeMake(FIXED_WIDTH, estimatedHeight)];

私が Github に付けたものは、一定幅のセルを使用し、動的タイプをサポートしているため、システムフォントサイズが変更されるとセルの高さが更新されます。

4
Daniel Galasko

はい、プログラムで自動レイアウトを使用して、ストーリーボードまたはxibで制約を設定することで実行できます。幅を一定に保ち、高さを以上に設定するには、制約を追加する必要があります。
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatic/

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
これが参考になり、問題が解決することを願っています。

0
Dhaivat Vyas