我将水平UICollectionView
固定在UITableViewCell
的所有边缘上。集合视图中的项目是动态调整大小的,我想使表格视图的高度等于最高的集合视图单元格的高度。
视图的结构如下:
UITableView
UITableViewCell
UICollectionView
UICollectionViewCell
UICollectionViewCell
UICollectionViewCell
class TableViewCell: UITableViewCell {
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: self.frame, collectionViewLayout: layout)
return collectionView
}()
var layout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = CGSize(width: 350, height: 20)
layout.scrollDirection = .horizontal
return layout
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let nib = UINib(nibName: String(describing: CollectionViewCell.self), bundle: nil)
collectionView.register(nib, forCellWithReuseIdentifier: CollectionViewCellModel.identifier)
addSubview(collectionView)
// (using SnapKit)
collectionView.snp.makeConstraints { (make) in
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.top.equalToSuperview()
make.bottom.equalToSuperview()
}
}
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 layout.collectionViewContentSize
}
override func layoutSubviews() {
super.layoutSubviews()
let spacing: CGFloat = 50
layout.sectionInset = UIEdgeInsets(top: 0, left: spacing, bottom: 0, right: spacing)
layout.minimumLineSpacing = spacing
}
}
class OptionsCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
return contentView.systemLayoutSizeFitting(CGSize(width: targetSize.width, height: 1))
}
}
class CollectionViewFlowLayout: UICollectionViewFlowLayout {
private var contentHeight: CGFloat = 500
private var contentWidth: CGFloat = 200
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if let attrs = super.layoutAttributesForElements(in: rect) {
// perform some logic
contentHeight = /* something */
contentWidth = /* something */
return attrs
}
return nil
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
}
在我的UIViewController
中,我有tableView.estimatedRowHeight = 500
和tableView.rowHeight = UITableView.automaticDimension
。
我正在尝试使systemLayoutSizeFitting
中的表格视图单元格的大小等于collectionViewContentSize
,但是在布局有机会将collectionViewContentSize
设置为正确值之前调用它。这些方法的调用顺序如下:
cellForRowAt
CollectionViewFlowLayout collectionViewContentSize: (200.0, 500.0) << incorrect/original value
**OptionsTableViewCell systemLayoutSizeFitting (200.0, 500.0) << incorrect/original value**
CollectionViewFlowLayout collectionViewContentSize: (200.0, 500.0) << incorrect/original value
willDisplay cell
CollectionViewFlowLayout collectionViewContentSize: (200.0, 500.0) << incorrect/original value
TableViewCell layoutSubviews() collectionViewContentSize.height: 500.0 << incorrect/original value
CollectionViewFlowLayout collectionViewContentSize: (200.0, 500.0) << incorrect/original value
CollectionViewFlowLayout layoutAttributesForElements
CollectionViewFlowLayout collectionViewContentSize: (450.0, 20.0)
CollectionViewFlowLayout layoutAttributesForElements
CollectionViewFlowLayout collectionViewContentSize: (450.0, 20.0)
CollectionViewCell systemLayoutSizeFitting
CollectionViewCell systemLayoutSizeFitting
CollectionViewCell systemLayoutSizeFitting
CollectionViewFlowLayout collectionViewContentSize: (450.0, 20.0)
CollectionViewFlowLayout layoutAttributesForElements
CollectionViewFlowLayout collectionViewContentSize: (450.0, 301.0) << correct size
CollectionViewFlowLayout collectionViewContentSize: (450.0, 301.0) << correct size
TableViewCell layoutSubviews() collectionViewContentSize.height: 301.0 << correct height
CollectionViewFlowLayout layoutAttributesForElements
CollectionViewFlowLayout collectionViewContentSize: (450.0, 301.0) << correct size
TableViewCell layoutSubviews() collectionViewContentSize.height: 301.0 << correct height
如何使表格视图单元格的高度等于集合视图的最高项目(由布局的collectionViewContentSize
表示)?
答案 0 :(得分:4)
有一些步骤可以做到这一点:
UICollectionViewCell
UICollectionView
UITableViewCell
bounds
已更改的高阶视图UICollectionViewCell
您在这里有两个选择:
-将自动布局与自定义界面元素(例如UIImageView
或UILabel
等一起使用)
-计算并提供您自己的尺寸
- 将自动布局与自定义界面元素一起使用:
如果您将estimatedItemsSize
设置为automatic
,并且使用自定义UI元素正确地布局了单元格子视图,它将自动进行实时调整。 但要注意,由于布局复杂,因此布局时间和滞后时间很长。特别是在较旧的设备上。
- 计算并提供您自己的尺寸:
可以设置单元格大小的最佳位置之一是intrinsicContentSize
。尽管还有其他地方可以执行此操作(例如,在flowLayout类等中),但是可以保证单元格不会与其他自定义UI元素发生冲突:
override open var intrinsicContentSize: CGSize { return /*CGSize you prefered*/ }
UICollectionView
任何UIScrollView
子类都可以对其contentSize
进行自我调整。由于collectionView和tableView内容的大小在不断变化,因此您应该通知layouter
(正式在任何地方都不称为布局器):
protocol CollectionViewSelfSizingDelegate: class {
func collectionView(_ collectionView: UICollectionView, didChageContentSizeFrom oldSize: CGSize, to newSize: CGSize)
}
class SelfSizingCollectionView: UICollectionView {
weak var selfSizingDelegate: CollectionViewSelfSizingDelegate?
override open var intrinsicContentSize: CGSize {
return contentSize
}
override func awakeFromNib() {
super.awakeFromNib()
if let floawLayout = collectionViewLayout as? UICollectionViewFlowLayout {
floawLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
} else {
assertionFailure("Collection view layout is not flow layout. SelfSizing may not work")
}
}
override open var contentSize: CGSize {
didSet {
guard bounds.size.height != contentSize.height else { return }
frame.size.height = contentSize.height
invalidateIntrinsicContentSize()
selfSizingDelegate?.collectionView(self, didChageContentSizeFrom: oldValue, to: contentSize)
}
}
}
因此,在覆盖intrinsicContentSize
之后,我们观察到contentSize的变化,并通知selfSizingDelegate
需要了解这一点。
UITableViewCell
可以通过在其tableView
中实现此单元格来调整大小:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
我们将在此单元格中放置一个selfSizingCollectionView
并实现它:
class SelfSizingTableViewCell: UITableViewCell {
@IBOutlet weak var collectionView: SelfSizingCollectionView!
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
return collectionView.frame.size
}
override func awakeFromNib() {
...
collectionView.selfSizingDelegate = self
}
}
因此我们符合委托并实现更新单元格的功能。
扩展名SelfSizingTableViewCell:CollectionViewSelfSizingDelegate { func collectionView(_ collectionView:UICollectionView,didChageContentSizeFrom oldSize:CGSize,to newSize:CGSize){ 如果让indexPath = indexPath { tableView?.reloadRows(at:[indexPath],带有:.none) }其他{ tableView?.reloadData() } } }
请注意,我为UIView
和UITableViewCell
编写了一些辅助扩展程序以访问父表视图:
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
extension UITableViewCell {
var tableView: UITableView? {
return (next as? UITableView) ?? (parentViewController as? UITableViewController)?.tableView
}
var indexPath: IndexPath? {
return tableView?.indexPath(for: self)
}
}
至此,您的集合视图将具有完全需要的高度(如果它是垂直的),但是您应该记住限制所有单元格的宽度,以免超出屏幕宽度。
扩展名SelfSizingTableViewCell:UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: frame.width, height: heights[indexPath.row])
}
通知heights
吗?在这里您应该缓存(手动/自动)所有单元格的计算高度,以免延迟。 (出于测试目的,我会随机生成所有这些文件)
已经涵盖了最后两个步骤,但是您应该考虑有关案件的一些重要事项:
1-Autolayout可能是史诗般的性能杀手,如果您要求它为您计算所有单元格大小,但这是可靠的。
2-您必须缓存所有大小并获得最高的大小,然后将其返回到tableView(:,heightForRowAt:)
中(不再需要调整tableViewCell
的大小)
3-考虑到最高的可能比整个tableView
高,因此2D滚动会很奇怪。
4-如果您重复使用包含collectionView
或tableView
的单元格,则滚动偏移量,inset和许多其他属性的行为会超出您的预期。
这是您可能遇到的最复杂的布局之一,但是一旦习惯了,它就会变得简单。
答案 1 :(得分:2)
我设法通过CollectionView的高度限制参考(不过我不使用SnapKit,也不使用programaticUI)来实现您想要的功能。
这是我的做法,可能会有所帮助:
NSLayoutConstraint
,该UICollectionView已经具有一个topAnchor
和一个bottomAnchor
等于其父视图class ChatButtonsTableViewCell: UITableViewCell {
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var buttonsCollectionView: UICollectionView!
@IBOutlet weak var buttonsCollectionViewHeightConstraint: NSLayoutConstraint! // reference
}
然后在我的UITableViewDelegate
实现中,在cellForRowAt上,将高度约束的常量设置为等于实际的collectionView框架,如下所示:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// let item = items[indexPath.section]
let item = groupedItems[indexPath.section][indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: ChatButtonsTableViewCell.identifier, for: indexPath) as! ChatButtonsTableViewCell
cell.chatMessage = nil // reusability
cell.buttonsCollectionView.isUserInteractionEnabled = true
cell.buttonsCollectionView.reloadData() // load actual content of embedded CollectionView
cell.chatMessage = groupedItems[indexPath.section][indexPath.row] as? ChatMessageViewModelChoicesItem
// >>> Compute actual height
cell.layoutIfNeeded()
let height: CGFloat = cell.buttonsCollectionView.collectionViewLayout.collectionViewContentSize.height
cell.buttonsCollectionViewHeightConstraint.constant = height
// <<<
return cell
}
我的假设是,您可以使用此方法将collectionView的高度设置为最大单元格的高度,将let height: CGFloat = cell.buttonsCollectionView.collectionViewLayout.collectionViewContentSize.height
替换为所需的实际高度。
此外,如果您使用的是水平UICollectionView,则考虑到contentSize.height等于最大单元格高度,您甚至不必更改此行。
它对我来说完美无缺,希望对您有帮助
编辑
仅考虑一下,您可能需要在此处和此处放置一些layoutIfNeeded()
和invalidateLayout()
。如果无法立即使用,我可以告诉您我将它们放在哪里。
编辑2
也忘了提到我实现了willDisplay
和estimatedHeightForRowAt
。
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let buttonCell = cell as? ChatButtonsTableViewCell {
cellHeights[indexPath] = buttonCell.buttonsCollectionView.collectionViewLayout.collectionViewContentSize.height
} else {
cellHeights[indexPath] = cell.frame.size.height
}
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 70.0
}
答案 2 :(得分:1)
它在您的代码中不起作用,因为在systemLayoutSizeFitting
完成布局之前调用了单元格的collectionView
。为了使其工作,需要在collectionView完成布局后更新表视图的布局。
我写了有关如何self sizing with collection view inside table view cell here的详细说明。我还包括sample codes here。以下是其要点:
UICollectionView
子类化,请覆盖layoutSubviews
。如果您将UIViewController
或UICollectionViewController
子类化,请覆盖viewDidLayoutSubviews
。systemLayoutSizeFitting
,以返回集合视图的contentSize
,您已经这样做了。tableView.beginUpdates()
和tableView.endUpdates()
将更新行高,而无需重新加载行。class CollectionViewController: UICollectionViewController {
// Set this action during initialization to get a callback when the collection view finishes its layout.
// To prevent infinite loop, this action should be called only once. Once it is called, it resets itself
// to nil.
var didLayoutAction: (() -> Void)?
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
didLayoutAction?()
didLayoutAction = nil // Call only once
}
}
class CollectionViewTableViewCell: UITableViewCell {
var collectionViewController: CollectionViewController!
override func awakeFromNib() {
super.awakeFromNib()
collectionViewController = CollectionViewController(nibName: String(describing: CollectionViewController.self), bundle: nil)
// Need to update row height when collection view finishes its layout.
collectionViewController.didLayoutAction = updateRowHeight
contentView.addSubview(collectionViewController.view)
collectionViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionViewController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
collectionViewController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
collectionViewController.view.topAnchor.constraint(equalTo: contentView.topAnchor),
collectionViewController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
private func updateRowHeight() {
DispatchQueue.main.async { [weak self] in
self?.tableView?.updateRowHeightsWithoutReloadingRows()
}
}
}
extension UITableView {
func updateRowHeightsWithoutReloadingRows(animated: Bool = false) {
let block = {
self.beginUpdates()
self.endUpdates()
}
if animated {
block()
}
else {
UIView.performWithoutAnimation {
block()
}
}
}
}
extension UITableViewCell {
var tableView: UITableView? {
return (next as? UITableView) ?? (parentViewController as? UITableViewController)?.tableView
}
}
extension UIView {
var parentViewController: UIViewController? {
if let nextResponder = self.next as? UIViewController {
return nextResponder
} else if let nextResponder = self.next as? UIView {
return nextResponder.parentViewController
} else {
return nil
}
}
}
还要注意,如果您的应用目标是iOS 14,请考虑使用新的UICollectionLayoutListConfiguration。它使集合视图的布局就像表格视图行一样。这是WWDC-20 video,显示了操作方法。
答案 3 :(得分:0)
几天前我也遇到了类似的问题,在$d = new DateTime('2019-06-09T12:56:52.081Z');
$d->setTime($d->format('H'), $d->format('i'), $d->format('s'), $d->format('u') - 1000);
echo $d->format('Y-m-d\TH:i:s.u\Z');
// 2019-06-09T12:56:52.080000Z
内获得了UICollectionView
的正确高度。这些解决方案均无效。我终于找到了resizing cells on device rotate
简而言之,我使用以下功能更新表格视图:
UITableViewCell
在- (void) reloadTableViewSilently {
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView beginUpdates];
[self.tableView endUpdates];
});
}
和viewWillAppear
中调用该函数
即使旋转设备,它也像吊饰一样工作。希望这会有所帮助。