web-dev-qa-db-ja.com

UITableViewCell内のUICollectionView-動的な高さ?

アプリケーション画面の1つでは、UICollectionViewの内側にUITableViewCellを配置する必要があります。このUICollectionViewには動的な数のアイテムが含まれるため、動的に計算する必要がある高さにもなります。ただし、埋め込みUICollectionViewの高さを計算しようとすると問題が発生します。

包括的なUIViewControllerはStoryboardsで作成され、自動レイアウトを使用しています。しかし、UITableViewCellの高さに基づいてUICollectionViewの高さを動的に増やす方法はわかりません。

誰もこれを達成する方法に関するヒントやアドバイスを提供できますか?

93
Shadowman

正解はYES、あなたはCANこれを行います。

数週間前にこの問題に出会いました。実際には思っているより簡単です。セルをNIB(またはストーリーボード)に配置し、それらを固定して、自動レイアウトにすべての作業を行わせる

次の構造を考えます:

TableView

TableViewCell

CollectionView

CollectionViewCell

CollectionViewCell

CollectionViewCell

[...可変数のセルまたは異なるセルサイズ]

解決策は、最初にcollectionViewCellサイズを計算し、次にコレクションビューのcontentSizeを計算し、それをセルのサイズとして使用するように自動レイアウトに指示することです。これはIView「マジックを行う」メソッドです:

-(void)systemLayoutSizeFittingSize:(CGSize)targetSize 
     withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority 
           verticalFittingPriority:(UILayoutPriority)verticalFittingPriority

ここでは、TableViewCellのサイズを設定する必要があります。この場合、これはCollectionViewのcontentSizeです。

CollectionViewCell

CollectionViewCellでは、モデルを変更するたびにレイアウトするようにセルに指示する必要があります(たとえば、テキストでUILabelを設定した後、セルを再度レイアウトする必要があります)。

- (void)bindWithModel:(id)model {
    // Do whatever you may need to bind with your data and 
    // tell the collection view cell's contentView to resize
    [self.contentView setNeedsLayout];
}
// Other stuff here...

TableViewCell

TableViewCellは魔法をかけます。 collectionViewへのアウトレットがあり、ICollectionViewFlowLayoutestimatedItemSizeを使用してcollectionViewセルの自動レイアウトを有効にします。

次に、トリックはsystemLayoutSizeFittingSize...メソッドでtableViewセルのサイズを設定することです。 (注:iOS8以降)

注:tableView -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath。のデリゲートセルの高さメソッドを使用しようとしましたが、自動レイアウトシステムがCollectionView contentSizeを計算するために手遅れですサイズ変更されたセルが間違っている場合があります。

@implementation TableCell

- (void)awakeFromNib {
    [super awakeFromNib];
    UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;

    // Configure the collectionView
    flow.minimumInteritemSpacing = ...;

    // This enables the magic of auto layout. 
    // Setting estimatedItemSize different to CGSizeZero 
    // on flow Layout enables auto layout for collectionView cells.
    // https://developer.Apple.com/videos/play/wwdc2014-226/
    flow.estimatedItemSize = CGSizeMake(1, 1);

    // Disable the scroll on your collection view
    // to avoid running into multiple scroll issues.
    [self.collectionView setScrollEnabled:NO];
}

- (void)bindWithModel:(id)model {
    // Do your stuff here to configure the tableViewCell
    // Tell the cell to redraw its contentView        
    [self.contentView layoutIfNeeded];
}

// THIS IS THE MOST IMPORTANT METHOD
//
// This method tells the auto layout
// You cannot calculate the collectionView content size in any other place, 
// because you run into race condition issues.
// NOTE: Works for iOS 8 or later
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority {

    // With autolayout enabled on collection view's cells we need to force a collection view relayout with the shown size (width)
    self.collectionView.frame = CGRectMake(0, 0, targetSize.width, MAXFLOAT);
    [self.collectionView layoutIfNeeded];

    // If the cell's size has to be exactly the content 
    // Size of the collection View, just return the
    // collectionViewLayout's collectionViewContentSize.

    return [self.collectionView.collectionViewLayout collectionViewContentSize];
}

// Other stuff here...

@end

TableViewController

TableViewControllerでtableViewセルの自動レイアウトシステムを有効にしてください。

- (void)viewDidLoad {
    [super viewDidLoad];

    // Enable automatic row auto layout calculations        
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    // Set the estimatedRowHeight to a non-0 value to enable auto layout.
    self.tableView.estimatedRowHeight = 10;

}

クレジット: @ rbarbera これを整理するのに役立ちました

103
Pablo Romeu

私の方法ははるかにシンプルだと思い、@ PabloRomeuによって提案されました。

手順1. UICollectionViewからUITableViewCell subclassへのアウトレットを作成します。ここには、UICollectionViewが配置されます。名前はcollectionViewになります

ステップ2. UICollectionViewの高さ制約にIBを追加し、UITableViewCell subclassへのアウトレットも作成します。名前はcollectionViewHeightになります。

ステップ3. tableView:cellForRowAtIndexPath:でコードを追加します。

// deque a cell
cell.frame = tableView.bounds;
[cell layoutIfNeeded];
[cell.collectionView reloadData];
cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;
75
Igor

テーブルビューとコレクションビューはいずれもUIScrollViewサブクラスであるため、コンテンツサイズの計算、セルの再利用などを試みるため、別のスクロールビュー内に埋め込むのは好ましくありません。

あらゆる目的でコレクションビューのみを使用することをお勧めします。

それをセクションに分割し、一部のセクションのレイアウトをテーブルビューとして、その他をコレクションビューとして「扱う」ことができます。結局のところ、テーブルビューでできるコレクションビューでは達成できないことは何もありません。

コレクションビュー「パーツ」の基本的なグリッドレイアウトがある場合は、通常のテーブルセルを使用してそれらを処理することもできます。それでもiOS 5のサポートが必要ない場合は、コレクションビューを使用することをお勧めします。

17
Rivera

上記のPablo Romeuの回答( https://stackoverflow.com/a/33364092/2704206 )は、私の問題に非常に役立ちました。しかし、これを私の問題のために機能させるには、いくつかのことを異なる方法で行わなければなりませんでした。まず、layoutIfNeeded()を頻繁に呼び出す必要はありませんでした。 collectionView関数のsystemLayoutSizeFittingで呼び出す必要がありました。

次に、テーブルビューセルのコレクションビューにパディングを与えるための自動レイアウト制約がありました。そのため、targetSize.widthの幅を設定するときに、collectionView.frameから先頭と末尾のマージンを差し引く必要がありました。また、戻り値CGSizeの高さに上下のマージンを追加する必要がありました。

これらの制約定数を取得するために、制約へのアウトレットを作成するか、定数をハードコーディングするか、識別子でルックアップするオプションがありました。カスタムテーブルビューセルクラスを簡単に再利用できるようにするために、3番目のオプションを選択することにしました。結局、これは私がそれを機能させるために必要なすべてでした:

class CollectionTableViewCell: UITableViewCell {

    // MARK: -
    // MARK: Properties
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionViewLayout?.estimatedItemSize = CGSize(width: 1, height: 1)
            selectionStyle = .none
        }
    }

    var collectionViewLayout: UICollectionViewFlowLayout? {
        return collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    }

    // MARK: -
    // MARK: UIView functions
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()

        let topConstraintConstant = contentView.constraint(byIdentifier: "topAnchor")?.constant ?? 0
        let bottomConstraintConstant = contentView.constraint(byIdentifier: "bottomAnchor")?.constant ?? 0
        let trailingConstraintConstant = contentView.constraint(byIdentifier: "trailingAnchor")?.constant ?? 0
        let leadingConstraintConstant = contentView.constraint(byIdentifier: "leadingAnchor")?.constant ?? 0

        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width - trailingConstraintConstant - leadingConstraintConstant, height: 1)

        let size = collectionView.collectionViewLayout.collectionViewContentSize
        let newSize = CGSize(width: size.width, height: size.height + topConstraintConstant + bottomConstraintConstant)
        return newSize
    }
}

識別子によって制約を取得するヘルパー関数として、次の拡張子を追加します。

extension UIView {
    func constraint(byIdentifier identifier: String) -> NSLayoutConstraint? {
        return constraints.first(where: { $0.identifier == identifier })
    }
}

注:ストーリーボードまたはこれらが作成されている場所で、これらの制約に識別子を設定する必要があります。定数が0でない限り、問題ではありません。また、Pabloの応答のように、UICollectionViewFlowLayoutをコレクションビューのレイアウトとして使用する必要があります。最後に、collectionView IBOutletをストーリーボードにリンクしてください。

上記のカスタムテーブルビューセルを使用して、コレクションビューを必要とする他のテーブルビューセルにサブクラス化し、UICollectionViewDelegateFlowLayoutおよびUICollectionViewDataSourceプロトコルを実装することができます。これが他の誰かに役立つことを願っています!

6
jmad8

おそらく私のバリアントが役に立つでしょう。過去2時間このタスクを決定しました。私はそれが100%正しいか、または最適であるふりをしません、しかし、私のスキルはまだ非常に小さいです、そして、私は専門家からコメントを聞きたいです。ありがとうございました。 1つの重要な注意:これは静的テーブルで機能します-現在の作業で指定されています。そのため、私が使用するのはviewWillLayoutSubviews of tableViewのみです。そしてもう少し。

private var iconsCellHeight: CGFloat = 500 

func updateTable(table: UITableView, withDuration duration: NSTimeInterval) {
    UIView.animateWithDuration(duration, animations: { () -> Void in
        table.beginUpdates()
        table.endUpdates()
    })
}

override func viewWillLayoutSubviews() {
    if let iconsCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 1)) as? CategoryCardIconsCell {
        let collectionViewContentHeight = iconsCell.iconsCollectionView.contentSize.height
        if collectionViewContentHeight + 17 != iconsCellHeight {
            iconsCellHeight = collectionViewContentHeight + 17
            updateTable(tableView, withDuration: 0.2)
        }
    }
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    switch (indexPath.section, indexPath.row) {
        case ...
        case (1,0):
            return iconsCellHeight
        default:
            return tableView.rowHeight
    }
}
  1. CollectionViewは2番目のセクションの最初の行にあることを知っています。
  2. 行の高さが17 pであるとします。コンテンツの高さよりも大きい。
  3. iconsCellHeightは、プログラムの開始時の乱数です(ポートレート形式では正確に392である必要がありますが、重要ではありません)。 collectionView + 17のコンテンツがこの数と等しくない場合、その値を変更します。この状況では、次回条件はFALSEになります。
  4. 結局、tableViewを更新します。私の場合、2つの操作の組み合わせ(拡張行のニース更新用);
  5. そしてもちろん、heightForRowAtIndexPathでコードに1行追加します。
2

コレクションビュークラスに静的メソッドを配置し、それが持つコンテンツに基づいてサイズを返します。次に、heightForRowAtIndexPathでそのメソッドを使用して、適切なサイズを返します。

また、これらの種類のviewControllerを埋め込むと、奇妙な動作が発生する可能性があることに注意してください。私は一度それをやったが、うまくいかなかった奇妙な記憶の問題を抱えていた。

2
InkGolem

私がこれまでに考え出した最も簡単なアプローチは、上記の@igorの答えへのクレジット、

あなたのtableviewcellクラスにこれを挿入してください

override func layoutSubviews() {
    self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}

そしてもちろん、セルのクラスのアウトレットでcollectionviewoutletを変更します

2
Osa

Pablo Romeuのソリューションの代替案は、Table View Cellで作業を行うのではなく、UICollectionView自体をカスタマイズすることです。

根本的な問題は、デフォルトではコレクションビューに固有のサイズがないため、使用するディメンションの自動レイアウトを通知できないことです。有用な固有のサイズを返すカスタムサブクラスを作成することで、これを改善できます。

UICollectionViewのサブクラスを作成し、次のメソッドをオーバーライドします

override func intrinsicContentSize() -> CGSize {
    self.layoutIfNeeded()

    var size = super.contentSize
    if size.width == 0 || size.height == 0 {
        // return a default size
        size = CGSize(width: 600, height:44)
    }

    return size
 }

override func reloadData() {
    super.reloadData()
    self.layoutIfNeeded()
    self.invalidateIntrinsicContentSize()
}

(関連するメソッドreloadSections、reloadItemsAtIndexPathsもreloadData()と同様の方法でオーバーライドする必要があります)

LayoutIfNeededを呼び出すと、コレクションビューでコンテンツサイズが再計算され、これが新しい組み込みサイズとして使用できるようになります。

また、Table View Controllerでビューサイズの変更(デバイスの回転など)を明示的に処理する必要があります

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    dispatch_async(dispatch_get_main_queue()) {
        self.tableView.reloadData()
    }
}
2
Dale

@ Igor からアイデアを得て、Swiftでの私のプロジェクトのためにこれに時間をかけます

あなたのこれを過ぎて

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    cell.frame = tableView.bounds
    cell.layoutIfNeeded()
    cell.collectionView.reloadData()
    cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
    cell.layoutIfNeeded()
    return cell
}

追加:セルのロード時にUICollectionViewが途切れる場合。

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {       
  //do dequeue stuff
  cell.layer.shouldRasterize = true
  cell.layer.rasterizationScale = UIScreen.main.scale
  return cell
}
1
Nazmul Hasan

私は最近同じ問題に直面していましたが、答えのほとんどすべての解決策を試してみましたが、いくつかはうまくいきましたが、他の人は@ PabloRomeアプローチに関する私の主な関心事ではありませんでした(コレクションビュー以外)高さおよび制約の高さを計算し、結果を返して自動レイアウトを正しく行う必要があります。コードで手動で計算するのは好きではありません。だからここに私のコードで手動の計算を行うことなく私のためにうまくいった解決策があります。

テーブルビューのcellForRow:atIndexPathで次の操作を行います。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    //initialize the the collection view data source with the data
    cell.frame = CGRect.zero
    cell.layoutIfNeeded()
    return cell
}

ここで何が起こるかは、コレクションビューの高さが計算された後に、TableViewセルの高さを強制的に調整することだと思います。 (データソースにcollectionView日付を提供した後)

私はすべての答えを読みました。これはすべての場合に役立つようです。

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()
        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width , height: 1)
        return collectionView.collectionViewLayout.collectionViewContentSize
}
1
SRP-Achiever

Pabloのソリューションは私にはあまりうまくいきませんでした。奇妙な視覚効果がありました(collectionViewが正しく調整されていません)。

うまくいったのは、layoutSubviews()の実行中にcollectionViewの高さの制約を(NSLayoutConstraintとして)collectionView contentSizeに調整することでした。これは、セルに自動レイアウトが適用されるときに呼び出されるメソッドです。

// Constraint on the collectionView height in the storyboard. Priority set to 999.
@IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!


// Method called by autolayout to layout the subviews (including the collectionView).
// This is triggered with 'layoutIfNeeded()', or by the viewController
// (happens between 'viewWillLayoutSubviews()' and 'viewDidLayoutSubviews()'.
override func layoutSubviews() {

    collectionViewHeightConstraint.constant = collectionView.contentSize.height
    super.layoutSubviews()
}

// Call `layoutIfNeeded()` when you update your UI from the model to trigger 'layoutSubviews()'
private func updateUI() {
    layoutIfNeeded()
}
0
Frédéric Adda