Good afternoon everyone. I have a collection with variable cell height. When scrolling a collection, the height of the content does not match the height of the cell. Tell me how to solve this problem?
This is how I create a collection.
// MARK: - UI Fabric.
private extension CollectionViewController {
func createCollectionView() -> UICollectionView {
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 16
layout.minimumLineSpacing = 16
layout.scrollDirection = .vertical
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.showsVerticalScrollIndicator = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}
}
This is delegate gor setting size cell.
// MARK: - CollectionView Flow Layout.
extension CollectionViewController: UICollectionViewDelegateFlowLayout {
/// Setting cell sizes.
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
delegateSettingCell.calculateCellSize(screenWidth: view.bounds.width, index: indexPath.row)
}
}
Here I am calculating the cell size.
func calculateCellSize(screenWidth: CGFloat, index: Int) -> CGSize {
let item = modelForDisplay[index].addonDetail.count
let heightCell = CGFloat((item * 44) + 44)
let sizeCellWidth = (view.bounds.width - 32)
return CGSize(width: sizeCellWidth, height: heightCell)
}
I made a size display by clicking on a cell. and after scrolling. the size does not change.
final class AddonCreatorCell: UICollectionViewCell {
// MARK: - Dependencies
var delegate: IHandlerAddonCreatorCellDelegate?
// MARK: - Public properties
static var reuseIdentifier: String = "AddonCreatorCell.cell"
// MARK: - Private properties
private lazy var textFields: [UITextField] = []
private lazy var viewDie = createView()
private lazy var gradientViewDie = createGradient(GradientColors.yellowGradient)
private lazy var labelTitle = createUILabel()
private lazy var switchShow = createSwitch()
private lazy var vStack = createStack()
private lazy var headerStack = createStack()
// MARK: - Initializator
override init(frame: CGRect) {
super.init(frame: frame)
addUIView()
setupConfiguration()
setupLayout()
}
convenience init(handlerAddonCreatorCellDelegate: IHandlerAddonCreatorCellDelegate?) {
self.init(frame: CGRect.zero)
self.delegate = handlerAddonCreatorCellDelegate
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Public methods
func reloadFrame(size: CGSize) {
self.gradientViewDie.frame.size = size
}
func reloadData(model: CreateAddon) {
// Here I delete unnecessary UIStask with content
vStack.subviews.forEach {
if $0 != headerStack {
$0.removeFromSuperview()
}
}
labelTitle.text = "\(model.title) \(model.addonDetail.count)"
model.isActive ? activeSwitch() : deactivateSwitch()
switchShow.setOn(model.isActive, animated: false)
for (index, element) in model.addonDetail.enumerated() {
createContent(item: element, index: index)
}
}
private func createContent(item: AddonDetail, index: Int) {
let convertToString = String(item.count)
let label = createUILabel()
let textField = createTextField(id: index, text: convertToString)
let hStack = createStack()
label.text = item.title
textFields.append(textField)
textField.delegate = self
textField.addTarget(self, action: #selector(self.myTextFieldChanged(_:)), for: .editingChanged)
hStack.axis = .horizontal
hStack.alignment = .center
hStack.distribution = .fillProportionally
hStack.addArrangedSubview(label)
hStack.addArrangedSubview(textField)
vStack.addArrangedSubview(hStack)
addConstraintTextField(textField)
}
}
You don't need to "re-build" your reusable cell every time...
Think of the stack view as "rows"
If you know the max number of rows any individual cell will have, create them in the cell's init.
Then, when you set the data in cellForItemAt
, show/hide the "rows" as needed. Something like this:
// show used "rows" and hide unused "rows"
for (i, v) in vStack.arrangedSubviews.enumerated() {
v.isHidden = !(i < model.addons.count)
}
If you don't know the potential max number of rows, create them as needed (again, when setting the cell data in cellForItemAt
:
// if we have fewer "rows" than addons, create new "rows"
while vStack.arrangedSubviews.count < model.addons.count {
// add a new textfield & switch
vStack.addArrangedSubview(...)
}
// show used "rows" and hide unused "rows"
for (i, v) in vStack.arrangedSubviews.enumerated() {
v.isHidden = !(i < model.addons.count)
}
When an arrangedSubview is hidden, it still exists, but the stack view treats it as if it isn't there.
Here's a complete example - based loosely on the code in your question:
struct Addon {
var text: String = ""
var selected: Bool = false
}
struct CreateAddon {
var title: String = ""
var addons: [Addon] = []
}
class CollectionViewController: UIViewController {
var myData: [CreateAddon] = []
var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView = createCollectionView()
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
collectionView.register(AddonCreatorCell.self, forCellWithReuseIdentifier: AddonCreatorCell.reuseIdentifier)
collectionView.dataSource = self
collectionView.delegate = self
view.backgroundColor = .init(red: 0.108, green: 0.379, blue: 0.129, alpha: 1.0)
collectionView.backgroundColor = .init(red: 0.108, green: 0.379, blue: 0.129, alpha: 1.0)
// create some sample data
let numRows: [Int] = [3, 5, 2, 6, 4, 3, 3, 4, 7, 5, 3, 4, 6, 5, 6, 2]
for (row, n) in numRows.enumerated() {
var theseAddons: [Addon] = []
for i in 0..<n {
let a: Addon = Addon(text: "User Text \(i)", selected: false)
theseAddons.append(a)
}
let cr = CreateAddon(title: "Addon Title \(row)", addons: theseAddons)
myData.append(cr)
}
}
}
extension CollectionViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return myData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: AddonCreatorCell.reuseIdentifier, for: indexPath) as! AddonCreatorCell
c.fillData(model: myData[indexPath.item])
// cell's "die" view width -- it has 8-points "spacing" on each side
c.wConstraint.constant = collectionView.frame.width - 16.0
return c
}
}
private extension CollectionViewController {
func createCollectionView() -> UICollectionView {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = .init(width: 300.0, height: 50.0)
layout.minimumInteritemSpacing = 16
layout.minimumLineSpacing = 16
layout.scrollDirection = .vertical
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.showsVerticalScrollIndicator = false
return collectionView
}
}
class TextFieldSwitch: UIView {
private let theTextField = UITextField()
private let theSwitch = UISwitch()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
let sv = UIStackView()
sv.spacing = 8
sv.addArrangedSubview(theTextField)
sv.addArrangedSubview(theSwitch)
self.addSubview(sv)
sv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0),
sv.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
sv.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0),
sv.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0.0),
])
theTextField.borderStyle = .bezel
// make sure the switch doesn't stretch
theSwitch.setContentHuggingPriority(.required, for: .horizontal)
}
public func fillData(d: Addon) {
theTextField.text = d.text
theSwitch.isOn = d.selected
}
}
final class AddonCreatorCell: UICollectionViewCell {
// we'll use this to make the cells the width of the collection view
public var wConstraint: NSLayoutConstraint!
// MARK: - Dependencies
//var delegate: IHandlerAddonCreatorCellDelegate?
// MARK: - Public properties
static var reuseIdentifier: String = "AddonCreatorCell.cell"
// MARK: - Private properties
private let viewDie = AddonGradientView()
private let labelTitle = UILabel() // createUILabel()
private let vStack = UIStackView() // createStack()
// MARK: - Initializator
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
vStack.axis = .vertical
vStack.spacing = 4
for v in [viewDie, labelTitle, vStack] {
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView
// this will be updated in cellForItemAt
// it will keep the cells the full width of the collection view
wConstraint = viewDie.widthAnchor.constraint(equalToConstant: 300.0)
let bConstraint = vStack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0)
// giving these two constraints less-than-required priority avoids auto-layout complaints
wConstraint.priority = .required - 1
bConstraint.priority = .required - 1
NSLayoutConstraint.activate([
viewDie.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
viewDie.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
viewDie.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
viewDie.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0),
labelTitle.topAnchor.constraint(equalTo: g.topAnchor, constant: 16.0),
labelTitle.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelTitle.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
vStack.topAnchor.constraint(equalTo: labelTitle.bottomAnchor, constant: 8.0),
vStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
vStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
bConstraint,
wConstraint,
])
viewDie.colors = [
.init(red: 0.918, green: 0.990, blue: 0.325, alpha: 1.0),
.init(red: 0.824, green: 0.894, blue: 0.242, alpha: 1.0),
.init(red: 0.918, green: 0.990, blue: 0.325, alpha: 1.0),
]
viewDie.layer.cornerRadius = 16
}
// MARK: - Public methods
func fillData(model: CreateAddon) {
// if we have fewer "rows" than addons, create new "rows"
while vStack.arrangedSubviews.count < model.addons.count {
vStack.addArrangedSubview(TextFieldSwitch())
}
// configure "rows"
for (row, addon) in zip(vStack.arrangedSubviews, model.addons) {
if let v = row as? TextFieldSwitch {
v.fillData(d: addon)
}
}
// show used "rows" and hide unused "rows"
for (i, v) in vStack.arrangedSubviews.enumerated() {
v.isHidden = !(i < model.addons.count)
}
labelTitle.text = model.title
}
}
class AddonGradientView: UIView {
public var colors: [UIColor] = [.gray, .white] {
didSet {
gradientLayer.colors = colors.map { $0.cgColor }
}
}
override class var layerClass: AnyClass { CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { layer as! CAGradientLayer }
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
self.backgroundColor = .clear
gradientLayer.colors = colors.map { $0.cgColor }
}
}
Looks like this:
Note: I didn't implement any of the data-updating when the user types in the text fields or toggles the switches ... I'll leave that up to you.
As a side note: for a single-column layout like this, a UITableView
is easier to manage than a UICollectionView