好?
我在实现 UITableViewCell 中的 UICollectionview 时遇到问题,因为该组件的高度计算无法正常工作,即使使用
updateConstraints()
并由委托 tableView.beginUpdates()
调用,有时也能正常工作,有时会渲染缺失的高度。
我的布局是一个包含很多单元格的表格,这些单元格在两个集合视图中有一些文本和项目(我使用 UICollectionView 因为这些项目的大小需要动态)
UITableViewCell的实现:
protocol CardTableViewCellDelegate: AnyObject {
func updateTableView()
}
class CardTableViewCell: UITableViewCell {
static let identifier: String = "CardTableViewCell"
lazy var cardView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 8
view.layer.masksToBounds = true
view.clipsToBounds = true
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.separator.cgColor
return view
}()
lazy var containerStackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 8
return stackView
}()
lazy var title: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .headline)
label.numberOfLines = 0
return label
}()
lazy var subtitle: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .subheadline)
label.numberOfLines = 0
return label
}()
lazy var sourceNew: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .footnote)
label.numberOfLines = 0
return label
}()
var assetsCollectionView: AssetsCollectionView = {
let collectionView = AssetsCollectionView()
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}()
lazy var benchmarksCollectionView: BenchmarksCollectionView = {
let collectionView = BenchmarksCollectionView()
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}()
weak var delegate: CardTableViewCellDelegate?
// MARK: - Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Methods
func configure(with card: Card) {
title.text = card.title
subtitle.text = card.subtitle
sourceNew.text = card.sourceNew
configureAssetsCollectionView(with: card.assets)
configurebenchmarksCollectionView(with: card.benchmarks)
}
private func configureAssetsCollectionView(with model: [Asset]?) {
if let assets = model {
assetsCollectionView.configure(with: assets)
containerStackView.addArrangedSubview(assetsCollectionView)
assetsCollectionView.updateConstraints()
delegate?.updateTableView()
}
}
private func configurebenchmarksCollectionView(with model: [Benchmark]?) {
if let benchmarks = model {
benchmarksCollectionView.configure(with: benchmarks)
containerStackView.addArrangedSubview(benchmarksCollectionView)
}
}
}
// MARK: - ViewCode
extension CardTableViewCell {
func setupView() {
setupLayout()
setupHierarchy()
setupConstrains()
}
func setupLayout() {
backgroundColor = .white
cardView.backgroundColor = .clear
}
func setupHierarchy() {
[title,
subtitle,
sourceNew].forEach(containerStackView.addArrangedSubview(_:))
cardView.addSubview(containerStackView)
contentView.addSubview(cardView)
}
func setupConstrains() {
NSLayoutConstraint.activate([
cardView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16),
containerStackView.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 8),
containerStackView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 8),
containerStackView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -8),
containerStackView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -8),
assetsCollectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 22),
benchmarksCollectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 22)
])
}
}
以及 UICollectionView:
import UIKit
class AssetsCollectionView: UICollectionView {
private var assets: [Asset] = []
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
print(contentSize)
return self.contentSize
}
init() {
let layout = LeftAlignedCollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.minimumLineSpacing = 8
layout.minimumInteritemSpacing = 8
super.init(frame: .zero, collectionViewLayout: layout)
delegate = self
dataSource = self
register(AssetCollectionViewCell.self,
forCellWithReuseIdentifier: AssetCollectionViewCell.identifier)
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with assets: [Asset]) {
self.assets = assets
reloadData()
}
}
extension AssetsCollectionView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return assets.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView
.dequeueReusableCell(withReuseIdentifier: AssetCollectionViewCell.identifier, for: indexPath) as? AssetCollectionViewCell else {
return UICollectionViewCell()
}
cell.configure(with: assets[indexPath.row])
return cell
}
}
那么,这个组件可以在不使用身高计算技巧的情况下自动计算你的身高吗?我尝试使用很多方法发送单元格来重新计算您的约束,但没有人起作用。 :(
我知道我可以使用项目的数量来计算高度,但我需要更强大的代码来处理可能的原因...任何人都可以帮助我吗?
本地重现的 github url:https://github.com/ramonfsk/MarketContextTimeline.git
尝试使用“自调整大小”集合视图时,您会遇到许多问题。
集合视图旨在基于框架布局单元格 - 而不是相反。
如果我们查看您的
LeftAlignedCollectionViewFlowLayout
(无论是否使用子类布局都会发生这种情况):
`layoutAttributesForElements(in rect: CGRect)`
被称为。
如果我们开始布局:
assetsCollectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 22)
rect
中的layoutAttributesForElements(in rect: CGRect)
将是:
(0.0, 0.0, 353.0, 22.0) // the width will vary
假设我们正在使用这些字符串:
"MGLU3", "IBVV11", "BBSA3", "Slightly Longer String",
"FIVE", "SIX", "SEVEN", "EIGHT",
"NINE", "TEN", "ELEVEN", "TWELVE",
"Hash Index Ethereum position replicated with a 100% accuracy",
"FOURTEEN", "FIFTEEN", "SIXTEEN", "SEVENTEEN",
目标是这样的:
布局开始看起来像这样:
让我们添加一个显示初始帧的红色矩形:
和一个绿色括号,显示 当前
collectionView.contentSize.height
:
因此,让我们展开集合视图框架以显示所有单元格:
请注意,
contentSize.height
确实不是反映了所有单元格...并且它可能会根据实际单元格大小、初始帧等而有很大变化。我们没有得到有效的(出于我们的目的) contentSize.height
直到所有单元格 都已由集合视图布局。
即使我们跳过一堆圈子来“强制”渲染所有单元格,我们仍然存在计时问题......集合视图在知道其宽度之前无法布局单元格,并且因为它嵌入在对于表格视图单元格,我们必须进行“回调”以自动调整单元格高度。
此外——让我们使用您的代码,但显式设置集合视图的高度以显示所有单元格:
看起来不错——除了单元格被重用...所以让我们上下滚动一下:
是的,显然不可接受。
所以...我建议使用
UIView
子类来自行布局标签。
总体逻辑是:
.systemLayoutSizeFitting(...)
获取“项目”的大小我们将从“填充标签视图”开始,它大致相当于一个单元格:
class PaddedLabelView: UIView {
let theLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
theLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(theLabel)
let edgeConstraints: [NSLayoutConstraint] = [
theLabel.topAnchor.constraint(equalTo: topAnchor, constant: 2.0),
theLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
theLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
theLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -2.0),
]
// this prevents auto-layout complaints
edgeConstraints[2].priority = .required - 1
edgeConstraints[3].priority = .required - 1
NSLayoutConstraint.activate(edgeConstraints)
// properties
theLabel.numberOfLines = 0
theLabel.textColor = .white
theLabel.font = .systemFont(ofSize: 12.0, weight: .bold)
backgroundColor = .systemBlue
layer.cornerRadius = 6.0
layer.masksToBounds = true
}
}
现在我们创建一个“排列视图”类:
class BasicArrangedViewsView: UIView {
public var theStrings: [String] = [] {
didSet {
// remove existing views
for v in labelViews { v.removeFromSuperview() }
labelViews = []
for str in theStrings {
let t = PaddedLabelView()
t.theLabel.text = str
addSubview(t)
labelViews.append(t)
}
calcFrames(bounds.width)
}
}
// horizontal space between label views
let interItemSpace: CGFloat = 8.0
// vertical space between "rows"
let lineSpace: CGFloat = 8.0
// we use these to set the intrinsic content size
private var myHeight: CGFloat = 0.0
private var myWidth: CGFloat = 0.0
private var myHC: NSLayoutConstraint!
private var labelViews: [PaddedLabelView] = []
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// initialize height constraint, but don't activate it yet
myHC = heightAnchor.constraint(equalToConstant: 0.0)
myHC.priority = .required - 1
}
func calcFrames(_ tagetWidth: CGFloat) {
// this can be called multiple times, and
// may be called before we have a frame
if tagetWidth == 0.0 {
return
}
var newWidth: CGFloat = 0.0
var newHeight: CGFloat = 0.0
var x: CGFloat = 0.0
var y: CGFloat = 0.0
var isMultiLine: Bool = false
var thisRect: CGRect = .zero
for thisView in labelViews {
// start with NOT needing to wrap
isMultiLine = false
// set the frame width to a very wide value, so we get the non-wrapped size
thisView.frame.size.width = 5000
thisView.layoutIfNeeded()
var sz: CGSize = thisView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
sz.width = ceil(sz.width)
sz.height = ceil(sz.height)
thisRect = .init(x: x, y: y, width: sz.width, height: sz.height)
// if this item is too wide to fit on the "row"
if thisRect.maxX > tagetWidth {
// if this is not the FIRST item on the row
// move down a row and reset x
if x > 0.0 {
x = 0.0
y = thisRect.maxY + lineSpace
}
thisRect = .init(x: x, y: y, width: sz.width, height: sz.height)
// if this item is still too wide to fit, that means
// it needs to wrap the text
if thisRect.maxX > tagetWidth {
isMultiLine = true
// this will give us the height based on max available width
sz = thisView.systemLayoutSizeFitting(.init(width: tagetWidth, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
sz.width = ceil(sz.width)
sz.height = ceil(sz.height)
// update the frame
thisView.frame.size = sz
thisView.layoutIfNeeded()
// this will give us the width needed for the wrapped text (instead of the max available width)
sz = thisView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
sz.width = ceil(sz.width)
sz.height = ceil(sz.height)
thisRect = .init(x: x, y: y, width: sz.width, height: sz.height)
}
}
// if we needed to wrap the text, adjust the next Y and reset X
if isMultiLine {
x = 0.0
y = thisRect.maxY + lineSpace
}
thisView.frame = thisRect
// update the max width var
newWidth = max(newWidth, thisRect.maxX)
// if we did NOT need to wrap lines, adjust the X
if !isMultiLine {
x += sz.width + interItemSpace
}
}
newHeight = thisRect.maxY
if myWidth != newWidth || myHeight != newHeight {
myWidth = newWidth
myHeight = newHeight
// don't activate the constraint if we're not in an auto-layout case
if self.translatesAutoresizingMaskIntoConstraints == false {
myHC.isActive = true
}
// update the height constraint constant
myHC.constant = myHeight
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
return .init(width: myWidth, height: myHeight)
}
override func invalidateIntrinsicContentSize() {
super.invalidateIntrinsicContentSize()
// walk-up the view hierarchy...
// this will handle self-sizing cells in a table or collection view, without
// the need to "call back" to the controller
var sv = superview
while sv != nil {
if sv is UITableViewCell || sv is UICollectionViewCell {
sv?.invalidateIntrinsicContentSize()
sv = nil
} else {
sv = sv?.superview
}
}
}
override var bounds: CGRect {
willSet {
if newValue.width != bounds.width {
calcFrames(newValue.width)
}
}
}
}
和一个示例控制器:
class BasicVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// sample strings
let strs: [String] = [
"MGLU3", "IBVV11", "BBSA3", "Slightly Longer String",
"FIVE", "SIX", "SEVEN", "EIGHT",
"NINE", "TEN", "ELEVEN", "TWELVE",
"Hash Index Ethereum position replicated with a 100% accuracy",
"FOURTEEN", "FIFTEEN", "SIXTEEN", "SEVENTEEN",
]
let aView = BasicArrangedViewsView()
aView.theStrings = strs
aView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(aView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
aView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
aView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
aView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// don't set a bottom or height constraint
])
// so we can see the view frame
aView.backgroundColor = .systemYellow
}
}
结果:
以及它在您的项目中的实现方式: