iOS - 如何使 UICollectionViewCell 根据其内容调整其高度? (包含一个 UITableView)
iOS - How to make an UICollectionViewCell adapt its height according to its content ? (containing an UITableView)
我不知道为什么设计可以适应其内容的单元格如此复杂。它应该不需要那么多代码,我仍然不明白为什么 UIKit
不能正确处理这个问题。
无论如何,这是我的问题(我已经编辑了整个post):
我有一个 UICollectionViewCell
,其中包含一个 UITableView
。
这是我的 sizeForItem
方法:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
var cellWidth: CGFloat = collectionView.bounds.size.width
var cellHeight: CGFloat = 0
let cellConfigurator = items[indexPath.item].cellConfigurator
if type(of: cellConfigurator).reuseId == "MoonCollectionViewCell" {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: type(of: cellConfigurator).reuseId, for: indexPath) as? MoonCollectionViewCell {
cell.contentView.layoutIfNeeded()
let size = cell.selfSizedTableView.intrinsicContentSize
cellHeight = size.height
}
}
return CGSize.init(width: cellWidth, height: cellHeight)
}
sizeForItem
在cellForItem
之前被调用,这就是layoutIfNeeded
的原因,因为我无法得到正确的intrinsic content size
。
我已经按照建议删除了 XIB,并在 Storyboard 中设计了我的 UICollectionViewCell。
这是我在 Storyboard 中设计的 UICollectionViewCell(只有 UITableViewCell 是在 XIB 文件中设计的)
我只在 UICollectionViewCell 中添加了一个 UITableView。
我希望 UICollectionViewCell
根据 tableView
.
的高度调整其大小
现在这是我的 tableView
:
我创建了 UITableView
的子类(来自这个 post)
class SelfSizedTableView: UITableView {
var maxHeight: CGFloat = UIScreen.main.bounds.size.height
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded()
}
override var intrinsicContentSize: CGSize {
let height = min(contentSize.height, maxHeight)
return CGSize(width: contentSize.width, height: height)
}
}
请注意,我有 disabled scrolling
,tableView 单元格有 dynamic prototype
,样式是 grouped
。
编辑: 检查配置方法,它来自我用来以通用方式配置所有 UICollectionViewCell
的协议
func configure(data: [MoonImages]) {
selfSizedTableView.register(UINib.init(nibName: "MoonTableViewCell", bundle: nil), forCellReuseIdentifier: "MoonTableViewCell")
selfSizedTableView.delegate = self
selfSizedTableView.dataSource = moonDataSource
var frame = CGRect.zero
frame.size.height = .leastNormalMagnitude
selfSizedTableView.tableHeaderView = UIView(frame: frame)
selfSizedTableView.tableFooterView = UIView(frame: frame)
selfSizedTableView.maxHeight = 240.0
selfSizedTableView.estimatedRowHeight = 40.0
selfSizedTableView.rowHeight = UITableView.automaticDimension
moonDataSource.data.addAndNotify(observer: self) { [weak self] in
self?.selfSizedTableView.reloadData()
}
moonDataSource.data.value = data
}
仅供参考 dataSource是一个自定义的dataSource,具有动态值(泛型)和观察者模式,当数据重新加载collection/tableView已设置。
我在启动应用程序时也有此警告。
[CollectionView] An attempt to update layout information was detected
while already in the process of computing the layout (i.e. reentrant
call). This will result in unexpected behaviour or a crash. This may
happen if a layout pass is triggered while calling out to a delegate.
关于我应该如何处理这个问题有什么提示或建议吗?
因为我遇到了一个奇怪的行为,就像我的 sizeForItem 使用随机值一样。 UICollectionViewCell
高度与我的 UITableView
内部内容大小高度不一样 .
如果我的 UITableView
中有 2 行,UICollectionView
在此大小下并不总是相等。我真的不知道如何实现这个...
我应该invalideLayout
吗?
这确实很棘手,但我找到了解决这个问题的有效方法。据我所知,这是我从一个聊天应用程序中得到的,消息气泡大小是动态的。
开始吧:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// Minimum size
let frame = CGRect(x: 0, y: 0, width: view.frame.width - 30, height: 0)
let cell = MoonCollectionViewCell()
// Fill it with the content it will have in the actual cell,
// cell.content is just an example
let cell.content = items[indexPath.item]
cell.layoutIfNeeded()
// Define the maximum size it can be
let targetSize = CGSize(width: view.frame.width - 30, height: 240)
let estimatedSize = cell.systemLayoutSizeFittingSize(tagetSize)
return CGSize(width: view.frame.width - 30, height: estimatedSize.height)
}
它基本上做的是定义一个最小框架和目标大小。然后通过调用 systemLayoutSizeFittingSize,将单元格调整到最佳大小,但不大于 targetSize。
根据您的需要调整代码,但这应该可行。
我试图在发布的代码中找到罪魁祸首,但似乎有很多活动部分。所以,我会尝试给出一些提示,希望能有所帮助。
理论上(iOS 12 有警告),自调整 UICollectionViewCells 应该不难。您基本上可以将 collectionViewLayout.estimedItemSize
设置为任何值(首选以下常量),如下所示:
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
然后你必须确保单元格中的约束以一种可以自行调整大小的方式设置;即自动布局可以计算单元格的宽度和高度。您正在提供 tableView 的 intrinsicContentSize
,它被其所有四个端的超级视图包裹,所以这应该没问题。
如上所示设置 estimatedItemSize
后,不应实现返回大小的委托方法:
func collectionView(_: UICollectionView, layout: UICollectionViewLayout, sizeForItemAt: IndexPath) -> CGSize
可在此处找到快速教程以供进一步参考:https://medium.com/@wasinwiwongsak/uicollectionview-with-autosizing-cell-using-autolayout-in-ios-9-10-84ab5cdf35a2
正如我所说,理论上应该不难,但单元格自动调整大小似乎在 iOS 12 上损坏,请参阅此处
如果我处在你的位置,我会重新开始,逐步增加复杂性:
- 尝试实现自动调整大小的单元格,可能使用简单的 UIView 和覆盖
intrinsicContentSize
;可能通过使用 iOS 11.4 SDK 来排除与 iOS 12 相关的问题(最简单的方法是下载最新的 Xcode 9 并从那里开始工作);如果不可能,请在此步骤中执行 iOS 12 个修复
- 用 table 视图替换简单视图(每个视图也可能具有动态大小)
- 执行table查看重新加载数据流,即动态调整大小功能
- 如果一切正常,执行 iOS 12 修复并迁移到 iOS 12
希望这对您有所帮助。
顺便说一句,控制台中的警告可能是由于在委托方法中调用了 layoutIfNeeded()
。它会立即触发布局传递,而这是在 UICollectionView
收集完所有尺寸后完成的。
也许这不是您想要的答案,但这是我的两分钱。对于您的特定要求,更好的解决方案是远离 UITableView
,并使用 UIStackView
或您的自定义容器视图。
原因如下:
UITableView
是 UIScrollView
的子 class,但由于您禁用了它的滚动功能,因此不需要 UIScrollView
.
UITableView
主要是用来重用cell,提高性能,让代码更有条理。但是由于您将其设置为与其内容大小一样大,none 的单元格将被重复使用,因此 UITableView
的功能没有得到任何利用。
因此,实际上您不需要也不应在 UICollectionViewCell
中使用 UITableView
或 UIScrollView
来满足您的要求。
如果您同意以上部分,以下是我们实践中的一些经验教训:
- 我们总是将大部分底层视图和代码逻辑(主要是数据组装)移动到基于
UIView
的自定义视图中,而不是直接放入 UITableViewCell
或 UICollectionViewCell
。然后将其添加到 UITableViewCell
或 UICollectionViewCell
的 contentView
并设置约束。有了这样的结构,我们就可以在更多的场景中复用我们自定义的视图。
- 对于与您类似的要求,我们将创建一个工厂 class 来创建 "rows",类似于您为
UITableView
创建 "cells" 的方式,将它们添加到垂直 UIStackView
,创建决定 UIStackView
宽度的约束。自动布局会处理剩下的事情。
- 在使用
UICollectionViewCell
时,要计算所需的高度,在单元格的 preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes)
函数中,您可以使用 contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
来计算高度,进行一些检查并 return。另外,请记住在 UICollectionView
的宽度发生变化时使布局无效。
我不知道为什么设计可以适应其内容的单元格如此复杂。它应该不需要那么多代码,我仍然不明白为什么 UIKit
不能正确处理这个问题。
无论如何,这是我的问题(我已经编辑了整个post):
我有一个 UICollectionViewCell
,其中包含一个 UITableView
。
这是我的 sizeForItem
方法:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
var cellWidth: CGFloat = collectionView.bounds.size.width
var cellHeight: CGFloat = 0
let cellConfigurator = items[indexPath.item].cellConfigurator
if type(of: cellConfigurator).reuseId == "MoonCollectionViewCell" {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: type(of: cellConfigurator).reuseId, for: indexPath) as? MoonCollectionViewCell {
cell.contentView.layoutIfNeeded()
let size = cell.selfSizedTableView.intrinsicContentSize
cellHeight = size.height
}
}
return CGSize.init(width: cellWidth, height: cellHeight)
}
sizeForItem
在cellForItem
之前被调用,这就是layoutIfNeeded
的原因,因为我无法得到正确的intrinsic content size
。
我已经按照建议删除了 XIB,并在 Storyboard 中设计了我的 UICollectionViewCell。
这是我在 Storyboard 中设计的 UICollectionViewCell(只有 UITableViewCell 是在 XIB 文件中设计的)
我只在 UICollectionViewCell 中添加了一个 UITableView。
我希望 UICollectionViewCell
根据 tableView
.
现在这是我的 tableView
:
我创建了 UITableView
的子类(来自这个 post)
class SelfSizedTableView: UITableView {
var maxHeight: CGFloat = UIScreen.main.bounds.size.height
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded()
}
override var intrinsicContentSize: CGSize {
let height = min(contentSize.height, maxHeight)
return CGSize(width: contentSize.width, height: height)
}
}
请注意,我有 disabled scrolling
,tableView 单元格有 dynamic prototype
,样式是 grouped
。
编辑: 检查配置方法,它来自我用来以通用方式配置所有 UICollectionViewCell
func configure(data: [MoonImages]) {
selfSizedTableView.register(UINib.init(nibName: "MoonTableViewCell", bundle: nil), forCellReuseIdentifier: "MoonTableViewCell")
selfSizedTableView.delegate = self
selfSizedTableView.dataSource = moonDataSource
var frame = CGRect.zero
frame.size.height = .leastNormalMagnitude
selfSizedTableView.tableHeaderView = UIView(frame: frame)
selfSizedTableView.tableFooterView = UIView(frame: frame)
selfSizedTableView.maxHeight = 240.0
selfSizedTableView.estimatedRowHeight = 40.0
selfSizedTableView.rowHeight = UITableView.automaticDimension
moonDataSource.data.addAndNotify(observer: self) { [weak self] in
self?.selfSizedTableView.reloadData()
}
moonDataSource.data.value = data
}
仅供参考 dataSource是一个自定义的dataSource,具有动态值(泛型)和观察者模式,当数据重新加载collection/tableView已设置。
我在启动应用程序时也有此警告。
[CollectionView] An attempt to update layout information was detected while already in the process of computing the layout (i.e. reentrant call). This will result in unexpected behaviour or a crash. This may happen if a layout pass is triggered while calling out to a delegate.
关于我应该如何处理这个问题有什么提示或建议吗?
因为我遇到了一个奇怪的行为,就像我的 sizeForItem 使用随机值一样。 UICollectionViewCell
高度与我的 UITableView
内部内容大小高度不一样 .
如果我的 UITableView
中有 2 行,UICollectionView
在此大小下并不总是相等。我真的不知道如何实现这个...
我应该invalideLayout
吗?
这确实很棘手,但我找到了解决这个问题的有效方法。据我所知,这是我从一个聊天应用程序中得到的,消息气泡大小是动态的。
开始吧:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// Minimum size
let frame = CGRect(x: 0, y: 0, width: view.frame.width - 30, height: 0)
let cell = MoonCollectionViewCell()
// Fill it with the content it will have in the actual cell,
// cell.content is just an example
let cell.content = items[indexPath.item]
cell.layoutIfNeeded()
// Define the maximum size it can be
let targetSize = CGSize(width: view.frame.width - 30, height: 240)
let estimatedSize = cell.systemLayoutSizeFittingSize(tagetSize)
return CGSize(width: view.frame.width - 30, height: estimatedSize.height)
}
它基本上做的是定义一个最小框架和目标大小。然后通过调用 systemLayoutSizeFittingSize,将单元格调整到最佳大小,但不大于 targetSize。
根据您的需要调整代码,但这应该可行。
我试图在发布的代码中找到罪魁祸首,但似乎有很多活动部分。所以,我会尝试给出一些提示,希望能有所帮助。
理论上(iOS 12 有警告),自调整 UICollectionViewCells 应该不难。您基本上可以将 collectionViewLayout.estimedItemSize
设置为任何值(首选以下常量),如下所示:
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
然后你必须确保单元格中的约束以一种可以自行调整大小的方式设置;即自动布局可以计算单元格的宽度和高度。您正在提供 tableView 的 intrinsicContentSize
,它被其所有四个端的超级视图包裹,所以这应该没问题。
如上所示设置 estimatedItemSize
后,不应实现返回大小的委托方法:
func collectionView(_: UICollectionView, layout: UICollectionViewLayout, sizeForItemAt: IndexPath) -> CGSize
可在此处找到快速教程以供进一步参考:https://medium.com/@wasinwiwongsak/uicollectionview-with-autosizing-cell-using-autolayout-in-ios-9-10-84ab5cdf35a2
正如我所说,理论上应该不难,但单元格自动调整大小似乎在 iOS 12 上损坏,请参阅此处
如果我处在你的位置,我会重新开始,逐步增加复杂性:
- 尝试实现自动调整大小的单元格,可能使用简单的 UIView 和覆盖
intrinsicContentSize
;可能通过使用 iOS 11.4 SDK 来排除与 iOS 12 相关的问题(最简单的方法是下载最新的 Xcode 9 并从那里开始工作);如果不可能,请在此步骤中执行 iOS 12 个修复 - 用 table 视图替换简单视图(每个视图也可能具有动态大小)
- 执行table查看重新加载数据流,即动态调整大小功能
- 如果一切正常,执行 iOS 12 修复并迁移到 iOS 12
希望这对您有所帮助。
顺便说一句,控制台中的警告可能是由于在委托方法中调用了 layoutIfNeeded()
。它会立即触发布局传递,而这是在 UICollectionView
收集完所有尺寸后完成的。
也许这不是您想要的答案,但这是我的两分钱。对于您的特定要求,更好的解决方案是远离 UITableView
,并使用 UIStackView
或您的自定义容器视图。
原因如下:
UITableView
是UIScrollView
的子 class,但由于您禁用了它的滚动功能,因此不需要UIScrollView
.UITableView
主要是用来重用cell,提高性能,让代码更有条理。但是由于您将其设置为与其内容大小一样大,none 的单元格将被重复使用,因此UITableView
的功能没有得到任何利用。
因此,实际上您不需要也不应在 UICollectionViewCell
中使用 UITableView
或 UIScrollView
来满足您的要求。
如果您同意以上部分,以下是我们实践中的一些经验教训:
- 我们总是将大部分底层视图和代码逻辑(主要是数据组装)移动到基于
UIView
的自定义视图中,而不是直接放入UITableViewCell
或UICollectionViewCell
。然后将其添加到UITableViewCell
或UICollectionViewCell
的contentView
并设置约束。有了这样的结构,我们就可以在更多的场景中复用我们自定义的视图。 - 对于与您类似的要求,我们将创建一个工厂 class 来创建 "rows",类似于您为
UITableView
创建 "cells" 的方式,将它们添加到垂直UIStackView
,创建决定UIStackView
宽度的约束。自动布局会处理剩下的事情。 - 在使用
UICollectionViewCell
时,要计算所需的高度,在单元格的preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes)
函数中,您可以使用contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
来计算高度,进行一些检查并 return。另外,请记住在UICollectionView
的宽度发生变化时使布局无效。