目次

目次

【Swift】TableViewCell の中に CollectionView を配置したときの TableViewCell の高さについて

アバター画像
澁谷太智
アバター画像
澁谷太智
最終更新日2021/07/09 投稿日2021/07/09

この記事を書くに至った経緯

TableViewCell の中に、CollectionViewを配置する手段を用いて開発をしていました。 その中で、CollectionViewは2列で表示されているのに、TableViewCellの長さはCollectionView1列分の長さになっているという事象が発生しました。 この問題の解決に2日要したので記事にしようと思いました。

再現gif

longTableViewCell.gif

作業環境

  • macOS Catalina
    • version 10.15.5
  • Xcode
    • version 12.3
  • iPhone12 Pro Max (シミュレーター)
    • iOS 14.3
  • Swift
    • version 5.3.2

TOC(Table Of Contents)

  • 発生した問題
  • 解決策
  • 高さが揃わない事象
    • TableViewCellの画面更新を行わない場合
    • 解決策コードを記述しなかった場合
  • layoutIfNeeded()の動き
  • まとめ
  • 最後に
  • 参考サイト一覧

発生した問題

TableViewCellの高さが決まるタイミングと、CollectionViewのセルの高さが決まるタイミングにズレがあるのか、期待通りの高さになりませんでした。

なんとなく、TableViewCellをlayoutIfNeeded()で画面更新すれば解決するのではないかと思っていましたが、そのタイミングが分からず苦戦しました。

原因については、現時点では推測です。解決策とともに、原因についても本記事でご紹介します。

解決策

TableViewCellのクラス内で、以下の記述を行うことで解決することができました。

self.layoutIfNeeded() を行うことで、TableViewCell自身の画面更新と、TableView配下のView(CollectionViewとその中)のレイアウト更新を行い、情報を新しいものに更新することができます。

以下の systemLayoutSizeFitting() というメソッドは、TableViewCellが作成された段階で読み込まれます。つまりこの時点で、親Viewに対して layoutIfNeeded() を行っておけば、レイアウト更新のフラグが立った時点で更新が行われる為、レイアウト崩れを起こさなくなります。

コード

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
    //TableViewCellのlayoutIfNeeded()
    self.layoutIfNeeded()

    let contentSize = self.collectionView.collectionViewLayout.collectionViewContentSize
    return CGSize(width: contentSize.width, height: contentSize.height)
}

gif

adjustedTableViewCell.gif

各メソッド内でprint()をして動きを見る

レイアウト更新が走るタイミングと、その他説明をコメント文で記載しました。

TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: cellForRowAt
TableView: heightForRowAt UITableView.automaticDimension -1.0
TableView: cellForRowAt ViewFrame (0.0, 0.0, 428.0, 926.0)
TableView: cellForRowAt HeaderViewFrame (0.0, 0.0, 428.0, 299.66666666666663)
TableView: cellForRowAt TableViewCellFrame (0.0, 0.0, 359.0, 591.0)
TableView: Put viewWidth Into TableViewCell Property
TableView: heightForRowAt UITableView.automaticDimension -1.0

TableViewCell: systemLayoutSizeFitting
TableViewCell: systemLayoutSizeFitting CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
TableViewCell: systemLayoutSizeFitting TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)

//TableViewCellのレイアウト更新が走ります。
layoutIfNeeded

//CollectionViewのレイアウト更新が走ります。
//このlayoutSubviews()で、CollectionView と TableViewCell の size(width・height) が揃います。
//これ以降、CollectionViewCell 二列分の幅が担保されます。
//故に、CollectionViewCell 二列分で計算が行われ、collectionViewContentSize の height が決定されます。
layoutSubviews

CollectionView: numberOfSections
CollectionView: numberOfSections CollectionViewFrame (0.0, 0.0, 428.0, 44.0)
CollectionView: numberOfSections TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: numberOfItemsInSection
CollectionView: numberOfItemsInSection CollectionViewFrame (0.0, 0.0, 428.0, 44.0)
CollectionView: numberOfItemsInSection TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: sizeForItemAt
CollectionView: sizeForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 44.0)
CollectionView: sizeForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
                            ・
                            ・ 中略
                            ・
CollectionView: sizeForItemAt
CollectionView: sizeForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 44.0)
CollectionView: sizeForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: insetForSectionAt
CollectionView: insetForSectionAt CollectionViewFrame (0.0, 0.0, 428.0, 44.0)
CollectionView: insetForSectionAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: minimumInteritemSpacingForSectionAt
CollectionView: minimumInteritemSpacingForSectionAt CollectionViewFrame (0.0, 0.0, 428.0, 44.0)
CollectionView: minimumInteritemSpacingForSectionAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: minimumLineSpacingForSectionAt
CollectionView: minimumLineSpacingForSectionAt CollectionViewFrame (0.0, 0.0, 428.0, 44.0)
CollectionView: minimumLineSpacingForSectionAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 44.0)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: cellForItemAt collectionViewContentSize: (428.0, 2044.0)
CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 44.0)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: cellForItemAt collectionViewContentSize: (428.0, 2044.0)

//CollectionViewCell のレイアウト更新が走ります。
layoutSubviews

TableViewCell: systemLayoutSizeFitting CollectionViewFrame (0.0, 0.0, 428.0, 44.0)
TableViewCell: systemLayoutSizeFitting TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
TableViewCell: systemLayoutSizeFitting collectionViewContentSize: (428.0, 2044.0)

//TableViewCell のレイアウト更新が走ります。
//ここで、CollectionView と TableViewCell の size(width・height) が更新されます。
layoutIfNeeded

//CollectionView のレイアウト更新が走ります。
layoutSubviews

CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 2044.0)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.6666666666665, 428.0, 2044.0)
CollectionView: cellForItemAt collectionViewContentSize: (428.0, 2044.0)
                            ・
                            ・ 中略
                            ・
CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 2044.0)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.6666666666665, 428.0, 2044.0)
CollectionView: cellForItemAt collectionViewContentSize: (428.0, 2044.0)

高さが揃わない事象

TableViewCellの画面更新を行わない場合

コード

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
    //TableViewCellではなく、CollectionViewに対して、layoutIfNeeded()を実行
    collectionView.layoutIfNeeded()

    let contentSize = self.collectionView.collectionViewLayout.collectionViewContentSize
    return CGSize(width: contentSize.width, height: contentSize.height)
 }

gif

longTableViewCell.gif

各メソッド内でprint()をして動きを見る

レイアウト更新が走るタイミングと、その他説明をコメント文で記載しました。

TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: cellForRowAt
TableView: heightForRowAt UITableView.automaticDimension -1.0
TableView: cellForRowAt ViewFrame (0.0, 0.0, 428.0, 926.0)
TableView: cellForRowAt HeaderViewFrame (0.0, 0.0, 428.0, 299.66666666666663)
TableView: cellForRowAt TableViewCellFrame (0.0, 0.0, 359.0, 591.0)
TableView: Put viewWidth Into TableViewCell Property
TableView: heightForRowAt UITableView.automaticDimension -1.0

TableViewCell: systemLayoutSizeFitting
TableViewCell: systemLayoutSizeFitting CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
TableViewCell: systemLayoutSizeFitting TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)

//この時点で、CollectionViewFrame の width が TableViewCellFrame の width より小さいので、CollectionViewCell 二列分の幅が担保されません。
//故に、一列分の高さが計算され、collectionViewContentSize の height が決定されます。

CollectionView: numberOfSections
CollectionView: numberOfSections CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
CollectionView: numberOfSections TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: numberOfItemsInSection
CollectionView: numberOfItemsInSection CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
CollectionView: numberOfItemsInSection TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: sizeForItemAt
CollectionView: sizeForItemAt CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
CollectionView: sizeForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
                            ・
                            ・ 中略
                            ・
CollectionView: sizeForItemAt
CollectionView: sizeForItemAt CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
CollectionView: sizeForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: insetForSectionAt
CollectionView: insetForSectionAt CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
CollectionView: insetForSectionAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: minimumInteritemSpacingForSectionAt
CollectionView: minimumInteritemSpacingForSectionAt CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
CollectionView: minimumInteritemSpacingForSectionAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: minimumLineSpacingForSectionAt
CollectionView: minimumLineSpacingForSectionAt CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
CollectionView: minimumLineSpacingForSectionAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: cellForItemAt collectionViewContentSize: (359.0, 4064.0)
CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: cellForItemAt collectionViewContentSize: (359.0, 4064.0)
CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
CollectionView: cellForItemAt collectionViewContentSize: (359.0, 4064.0)

TableViewCell: systemLayoutSizeFitting CollectionViewFrame (0.0, 0.0, 359.0, 591.0)
TableViewCell: systemLayoutSizeFitting TableViewCellFrame (0.0, 299.66666666666663, 428.0, 44.0)
TableViewCell: systemLayoutSizeFitting collectionViewContentSize: (359.0, 4064.0)

//TableViewCell のレイアウト更新が走ります。
layoutIfNeeded

//CollectionView のレイアウト更新が走ります。
//ここで、CollectionView と TableViewCell の size(width・height) が揃います。
layoutSubviews

//CollectionViewCell のレイアウト更新が走ります。
//二列文の幅が担保されたので、CollectionViewCell に更新フラグが立ち、更新が行われます。
layoutSubviews

CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 4064.0)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.6666666666665, 428.0, 4064.0)

//更新が行われたことにより、collectionViewContentSize の height が 二列分の高さに変わっています。
//これ以降、layoutIfNeeded()が行われないため、CollectionViewとTableViewCellの高さは、一列分の高さのままというわけです。
CollectionView: cellForItemAt collectionViewContentSize: (428.0, 2044.0)
                            ・
                            ・ 中略(Cellの個数分実行)
                            ・
CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 4064.0)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.6666666666665, 428.0, 4064.0)
CollectionView: cellForItemAt collectionViewContentSize: (428.0, 2044.0)

解決策コードを記述しなかった場合

スクリーンショット

Simulator Screen Shot - iPhone 12 Pro Max - 2020-12-22 at 15.16.09.png

各メソッド内でprint()をして動きを見る

レイアウト更新が走るタイミングと、その他説明をコメント文で記載しました。

TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection
TableView: numberOfSections
TableView: numberOfRowsInSection

TableView: cellForRowAt
TableView: heightForRowAt UITableView.automaticDimension -1.0
TableView: cellForRowAt ViewFrame (0.0, 0.0, 428.0, 926.0)
TableView: cellForRowAt HeaderViewFrame (0.0, 0.0, 428.0, 299.66666666666663)
TableView: cellForRowAt TableViewCellFrame (0.0, 0.0, 359.0, 591.0)
TableView: Put viewWidth Into TableViewCell Property
TableView: heightForRowAt UITableView.automaticDimension -1.0

//TableViewCell の ContentView の高さが曖昧なので、TableViewCell の frame を適応するという警告文
//TableViewCell にレイアウト更新フラグが立ちます。
2020-12-22 14:23:53.811492+0900 CollectionViewInTableViewCell[7926:7591780] [Warning] Warning once only: Detected a case where constraints ambiguously suggest a height of zero for a table view cell's content view. We're considering the collapse unintentional and using standard height instead. Cell: <CollectionViewInTableViewCell.TableViewCell: 0x7fa95602ba00; baseClass = UITableViewCell; frame = (0 299.667; 428 44); autoresize = W; layer = <CALayer: 0x6000029a4360>>

//TableViewCell の更新が走ります。
layoutIfNeeded

//CollectionView の更新が走ります。
layoutSubviews

CollectionView: numberOfSections
CollectionView: numberOfItemsInSection

//Cellのサイズを計算
CollectionView: sizeForItemAt
CollectionView: sizeForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 43.666666666666664)
CollectionView: sizeForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 43.666666666666664)
                            ・
                            ・ 中略
                            ・
CollectionView: sizeForItemAt
CollectionView: sizeForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 43.666666666666664)
CollectionView: sizeForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 43.666666666666664)
CollectionView: insetForSectionAt
CollectionView: insetForSectionAt CollectionViewFrame (0.0, 0.0, 428.0, 43.666666666666664)
CollectionView: insetForSectionAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 43.666666666666664)
CollectionView: minimumInteritemSpacingForSectionAt
CollectionView: minimumInteritemSpacingForSectionAt CollectionViewFrame (0.0, 0.0, 428.0, 43.666666666666664)
CollectionView: minimumInteritemSpacingForSectionAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 43.666666666666664)
CollectionView: minimumLineSpacingForSectionAt
CollectionView: minimumLineSpacingForSectionAt CollectionViewFrame (0.0, 0.0, 428.0, 43.666666666666664)
CollectionView: minimumLineSpacingForSectionAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 43.666666666666664)

//self.collectionView.collectionViewLayout.collectionViewContentSize が決定
//Viewの更新が入らないので、上記の minimumLineSpacingForSectionAt 時点でのframeで描画される事になります。

CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 43.666666666666664)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 43.666666666666664)
CollectionView: cellForItemAt collectionViewContentSize: (428.0, 2044.0)
CollectionView: cellForItemAt
CollectionView: cellForItemAt CollectionViewFrame (0.0, 0.0, 428.0, 43.666666666666664)
CollectionView: cellForItemAt TableViewCellFrame (0.0, 299.66666666666663, 428.0, 43.666666666666664)
CollectionView: cellForItemAt collectionViewContentSize: (428.0, 2044.0)
layoutSubviews

layoutIfNeeded()の動き

systemLayoutSizeFitting() をoverride して layoutIfNeeded() をしている事象を見てみると、自分自身(ここではtableViewCell)と配下のViewの更新が全て終わるまで、次の動きを待機していることがわかります。 調べてみると、あるサイトに、

この処理を呼んだ場合再描画処理が同期実行されます。その為、再描画が完了するまで呼び出し側の実行が止まります。

と記載してありました。

また、layoutIfNeeded()を行うと、親View → 子View の順番でレイアウトの更新が走ります。

参考サイト

まとめ

根本原因

いろいろな場所で print() をした結果、今回の原因は、タイミングではありませんでした

  • CollectionView の width を TableViewCell の width に合わせたか
  • CollectionViewCell二列分の幅が、サイズ計算が始まる前に担保されていたか

上記の2点だったように思います。

今回のコードでは、以下のように、CollectionViewCell の width を TableViewCell の width の約半分として設定しています。

let widthHeight = (viewWidth - insetWidth) / 2
return CGSize(width: widthHeight, height: widthHeight)

故に、CollectionView の width が、TableViewCell の width より狭かった場合、CollectionViewCell が二列収まらなくなり、一列分として計算を始めてしまいます。

解決策(再掲)

TableViewCell の中に、CollectionView を入れるときは、

  • systemLayoutSizeFitting()override
  • その中で TableViewCell の layoutIfNeeded() を実行

CollectionView の size を TableViewCell の size に更新してくれます。

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        //TableViewCellのlayoutIfNeeded()
        self.layoutIfNeeded()

        let contentSize = self.collectionView.collectionViewLayout.collectionViewContentSize
        return CGSize(width: contentSize.width, height: contentSize.height)
}

最後に

これから、UITableViewが使われなくなっていくと聞いていますが、もし CollectionView in TableViewCell という複雑なやり方をするのであれば、参考にしていただければと思います。

最後まで記事を読んで頂き、ありがとうございました。

参考サイト一覧

アバター画像

澁谷太智

目次