1. ホーム
  2. ios

[解決済み] UICollectionViewでオートレイアウトによるセルの1次元指定

2022-09-20 03:56:24

質問

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レイアウトのような他の状況に対応しません。

どのように解決するのですか?

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

一方、本当にデフォルトのUICollectionViewFlowLayoutできれいにできる方法があるのかというと、方法はないと思っています。iOS8のセルフサイズセルを使っても、一筋縄ではいきません。根本的な問題は、おっしゃるとおり、フローレイアウトの機構上、ある次元を固定して別の次元を対応させる方法がないことです。(さらに、たとえそれができたとしても、複数行のラベルをサイズ調整するために2つのレイアウトパスが必要になるため、さらに複雑になってしまいます。これは、自己サイズ化セルが systemLayoutSizeFittingSize への 1 回の呼び出しによってすべてのサイズ設定を計算したい方法と一致しないかもしれません)。

しかし、もしあなたがまだ、フローレイアウトでテーブルビューのようなレイアウトを作成したいのであれば、セルがそれ自身のサイズを決定し、コレクションビューの幅に自然に反応するように、もちろんそれは可能です。まだ、面倒な方法があります。私はそれを、"sizing cell"、つまり、コントローラがセルサイズの計算のためだけに保持する非表示の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のセッション「"Advanced Collection View"」のAppleのサンプルコードから引用しています)。

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

この方法は好きですか?いいえ! あまりにも複雑に感じますし、レイアウトを 2 回実行することになります。しかし、パフォーマンスが問題にならない限り、コードで 2 回レイアウトを定義するよりも、実行時に 2 回レイアウトを実行する方が良いと思います。

私の希望は、最終的にセルフサイズのセルが異なる方法で動作し、このすべてがよりシンプルになることです。

プロジェクト例 を表示します。

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

理論的には、iOS8 の新しい機能である "self-sizing cells" を使えば、このようなことは不要になるはずです。自動レイアウト (AL) を使用してセルを定義した場合、コレクション ビューは十分に賢く、セルのサイズとレイアウトを正しく設定できるはずです。実際には、複数行のラベルでこれを実現した例を見たことがありません。私が思うに この は、セルフサイズ・セルのメカニズムがまだバグがあるためだと思います。

しかし、私は、UILabels が基本的に 2 段階のレイアウト プロセスを必要とするという、Auto Layout とラベルの通常のトリックのためであると確信しています。セルフ サイズのセルで両方のステップを実行する方法は、私には明らかではありません。

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

そして preferredLayoutAttributesFittingAttributes についてはどうでしょう。?

preferredLayoutAttributesFittingAttributes: メソッドは赤信号だと思います。あれはあくまで新しいセルフサイジングセルの機構で使うためにあるのです。だから、そのメカニズムが信頼できない以上、これは答えにはならない。

そして、systemlayoutSizeFittingSize: はどうなっているのでしょうか?

確かにdocsは分かりにくいですね。

のドキュメントは systemLayoutSizeFittingSize:systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: を渡すだけでよいことを示唆しています。 UILayoutFittingCompressedSizeUILayoutFittingExpandedSize として targetSize . しかし、メソッドのシグネチャ自体、ヘッダコメント、関数の動作から、正確には targetSize パラメーターの正確な値に応答していることを示しています。

実際、もしあなたが UICollectionViewFlowLayoutDelegate.estimatedItemSize を設定すると、新しいセルフ サイズのセル メカニズムを有効にするために、その値は targetSize として渡されるようです。また UILabel.systemLayoutSizeFittingSize と全く同じ値が返されるようです。 UILabel.sizeThatFits . の引数が同じであることを考えると、これは疑わしい。 systemLayoutSizeFittingSize の引数は大まかな目標であることが想定されており sizeThatFits: の引数は最大外接サイズであると思われます。

その他のリソース

このような日常的な要件が研究リソースを必要とすると考えるのは悲しいことですが、私はそう考えています。良い例と議論があります。