この記事を書くに至った経緯
TableViewCell の中に、CollectionViewを配置する手段を用いて開発をしていました。
その中で、CollectionViewは2列で表示されているのに、TableViewCellの長さはCollectionView1列分の長さになっているという事象が発生しました。
この問題の解決に2日要したので記事にしようと思いました。
再現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
各メソッド内で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
各メソッド内で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) |
解決策コードを記述しなかった場合
スクリーンショット
各メソッド内で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 の順番でレイアウトの更新が走ります。
参考サイト
- Qiita – 「setNeedsDisplay」、「setNeedsLayout」、「layoutIfNeeded」、「layoutSubviews」の違い
- Qiita – UIKitのView表示ライフサイクルを理解する
- UIView が持つ描画・レイアウト更新系のメソッドメモ
まとめ
根本原因
いろいろな場所で 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 という複雑なやり方をするのであれば、参考にしていただければと思います。
最後まで記事を読んで頂き、ありがとうございました。
参考サイト一覧
- 解決策( systemLayoutSizeFitting() を override してその中で、TableViewCell の layoutIfNeeded() を実行する)
- layoutIfNeeded()の動き