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


113

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

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

簡単な例として、セル内の複数行のラベルを、セルの側面に配置する制約のあるものについて考えます。複数行のラベルは、テキストに合わせてさまざまな方法でサイズ変更できます。セルはコレクションビューの幅を埋め、それに応じて高さを調整する必要があります。代わりに、セルのサイズが不規則になり、セルのサイズがコレクションビューのスクロールできないサイズよりも大きい場合に、クラッシュが発生します。


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

このようなもの:

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

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

sizeこのメソッドのパラメーターは本当に2つの定数を渡す方法にすぎませんか?特定のサイズに適切な高さを取得するために期待する動作を実現する方法はありませんか?


代替ソリューション

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

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

回答:


135

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

一方、デフォルトのUICollectionViewFlowLayoutを使用してこれを行うクリーンな方法があるかどうかを本当に質問している場合は、方法がないと思います。iOS8のセルフサイジングセルを使用しても、簡単ではありません。あなたが言うように、根本的な問題は、フローレイアウトの機構が1つの次元を修正して別の次元を応答させる方法を提供しないことです。(さらに、可能であれば、複数行のラベルのサイズを設定するために2つのレイアウトパスが必要になると、さらに複雑になります。これは、systemLayoutSizeFittingSizeへの1回の呼び出しでセルフサイズセルがすべてのサイズを計算する方法と一致しない場合があります。)

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

(このコードは、「Advanced Collection View」のWWDC2014セッションのサンプルコードのAppleサンプルコードを基にしています。)

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

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

私の希望は、最終的にはセルのサイズを変更するセルの動作が異なり、これによりすべてがはるかに簡単になることです。

仕事でそれを示すサンプルプロジェクト

しかし、なぜセルフサイジングセルを使用しないのですか?

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

しかし、それは主に自動レイアウトとラベルの通常の扱いやすさによるものだと思います。つまり、UILabelsは基本的に2段階のレイアウトプロセスを必要とするということです。セルのサイズをセルで両方の手順を実行する方法はわかりません。

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

そして、preferredLayoutAttributesFittingAttributes:はどうですか?

そのpreferredLayoutAttributesFittingAttributes:方法は赤ニシンだと思います。それだけが、新しいセルフサイジングセルメカニズムで使用されます。したがって、そのメカニズムが信頼できない限り、これは答えではありません。

そして、systemlayoutSizeFittingSize :?

あなたは正しい、ドキュメントは混乱しています。

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

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

その他のリソース

そのような日常的な必要条件が「研究リソース」を必要とするべきだと考えるのは悲しいことですが、私はそう思うと思います。良い例と議論は:


フレームを元のフレームに戻すのはなぜですか?
vrwim

これを行うのは、呼び出したビューのレイアウト状態に影響を与えずに、ビューの適切なレイアウトサイズを決定するためにのみ関数を使用できるようにするためです。実際には、このビューを目的のためだけに保持している場合、これは無関係です。レイアウト計算を行います。また、私がここで行うことは不十分です。レイアウトを実行するとサブビューのレイアウトが変更される可能性があるため、フレームのみを復元しても、実際には以前の状態に復元されません。
藻類、2015

1
これは、ios6以来人々がしている何かのためのそのような混乱です。アップルは常に何らかの方法で出てきますが、それは決して正しくありません。
GregP

1
viewDidLoadでサイズ変更セルを手動でインスタンス化し、その中に保持してカスタムプロパティを表示できます。たった1セルなので安いです。サイズ設定にのみ使用し、すべての操作を管理しているので、コレクションビューのセル再利用メカニズムに参加する必要がないため、コレクションビュー自体から取得する必要はありません。
藻類、

1
一体どのようにしてセルのサイズを変更することができますか?
Reuben Scratton 2016年

50

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

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
}

8
これは、私がセルフサイジングのトピックで見つけた最大の答えです。ジョーダンありがとう!
haik.ampardjian 2016年

1
これをiOS(9.3)で動作させることはできません。10では正常に動作します。アプリケーションは_updateVisibleCellsでクラッシュします(無限再帰?)。openradar.me/25303115
cristiano2lopes

@ cristiano2lopes私のiOS 9.3ではうまく機能します。適切な見積もりを提供していることを確認し、セルの制約が正しいサイズに解決されるように設定されていることを確認してください。
ジョーダンスミス

これは素晴らしい作品です。Xcode 8を使用していて、iOS 9.3.3(物理デバイス)でテストしましたが、クラッシュしません!よく働く。見積もりが正しいことを確認してください。
2016年

1
@JordanSmithこれはiOS 10では機能しません。サンプルのデモはありますか?私はその内容に基づいてコレクションビューのセルの高さを行うために多くのことを試みましたが、何もしていません。私はiOSの10で自動レイアウトを使用し、迅速な3を使用しています
Ekta Padaliyaを

6

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

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

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

コレクションビューレイアウトデリゲートを実装します(ほとんどの場合、ビューコントローラーで)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;
}

iOS9では、セルが複数行を含みUILabel、その改行がセルの固有の高さに影響を与えるときに、このより簡単な方法が正しく機能するかどうかを知っていますか?これは、私が説明するすべてのバックフリップを必要としていた一般的なケースです。私の答え以来、iOSがそれを修正したかどうか疑問に思っています。

2
@algal悲しいことに、答えはノーです。
ジェームスク

4

優先レイアウト属性で幅を固定してみてください:

- (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です。システムのフォントサイズが変更されるとセルの高さが更新されます。


このスニペットでは、systemLayoutSizeFittingSize:を呼び出します。どこかにpreferredMaxLayoutWidthも設定せずにこれを機能させることができましたか?私の経験では、preferredMaxLayoutWidthを設定していない場合は、追加の手動レイアウトを行う必要があります。たとえば、sizeThatFits:を他の場所で定義された自動レイアウト制約のロジックを再現する計算でオーバーライドするなどです。
藻類、2014年

@algal preferredMaxLayoutWidthはUILabelのプロパティですか?それがどのように関連しているのか完全にわかりませんか?
Daniel Galasko、2014年

おっとっと!良い点ではありません!UITextViewでこれを処理したことがないので、APIが異なることに気づきませんでした。UITextView.systemLayoutSizeFittingSizeには固有のコンテンツサイズがないため、フレームを尊重しているようです。ただし、UILabel.systemLayoutSizeFittingSizeはフレームに固有のコンテンツサイズがあるため、フレームを無視します。そのため、ラップされた高さをALに公開するために、imitiveContentSizeを取得するには、preferredMaxLayoutWidthが必要です。UITextViewには2つのパスまたは手動レイアウトが必要なようですが、これを完全に理解しているとは思いません。
藻類

@algal私は同意することができます、UILabelを使用するときに私はいくつかの毛深い障害も経験しました。優先幅を設定すると問題が解決しますが、この幅はスーパービューに合わせて調整する必要があるため、より動的なアプローチを採用するとよいでしょう。私のプロジェクトのほとんどは、preferredプロパティを使用していません。常により深い問題があるため、私は通常、それをクイックフィックスとして使用します
Daniel Galasko

0

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

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
これが期待される参考になり、問題を解決してください。


3
これは絶対幅の値で機能するはずですが、セルを常にコレクションビューの全幅にしたい場合は機能しません。
マイケルウォーターフォール

@MichaelWaterfall AutoLayoutを使用して全幅で機能させることができました。セル内のコンテンツビューに幅の制約を追加しました。次にcellForItemAtIndexPath、制約を更新しますcell.constraintItemWidth.constant = UIScreen.main.bounds.width。私のアプリはポートレートのみです。おそらくreloadData、拘束を更新するために方向を変更した後です。
Flitskikker 2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.