Reputation: 515
I'm trying to set a UICollectionView as a horizontal menu, on top of another UICollectionView (but that's irrelevant).
Now my cells are basically composed of label, which are as you can imagine of different width. So I tried to calculate the size with this function :
func getSizeOfCell(string: String, font: UIFont) -> CGSize {
let textString = string as NSString
let textAttributes = [NSFontAttributeName: font]
let size = textString.boundingRect(with: CGSize(width: 320, height: 2000), options: .usesLineFragmentOrigin, attributes: textAttributes, context: nil)
return CGSize(width: size.width, height: size.height)
}
Which is not really doing a good job, as you can see :
The view containing the label are too big when the string is long.
I'm kind of forced to give sizeForItemAt a CGSize right ? I can't just ask my CollectionView to get automatically the size of a cell based on a label it contains ?
[edit] so here's my cell class :
class MenuCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
override var isSelected: Bool {
didSet {
labelTitre.textColor = isSelected ? UIColor.black : .lightGray
}
}
let labelTitre : UILabel = {
let label = UILabel()
label.textColor = .gray
return label
}()
func setupViews() {
backgroundColor = UIColor.white
addSubview(labelTitre)
addConstraintsWithFormat(format: "H:[v0]", views: labelTitre)
addConstraintsWithFormat(format: "V:[v0]", views: labelTitre)
addConstraint(NSLayoutConstraint(item: labelTitre, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0))
addConstraint(NSLayoutConstraint(item: labelTitre, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And here's how I construct my collectionView :
lazy var customCollectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
layout.estimatedItemSize = CGSize(width: 60, height: 30)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.collectionViewLayout = layout
cv.backgroundColor = UIColor.white
cv.dataSource = self
cv.delegate = self
return cv
}()
with also in the viewDidLoad() of my viewController:
customCollectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)
addSubview(customCollectionView)
addConstraintsWithFormat(format: "V:|[v0]|", views: customCollectionView)
addConstraintsWithFormat(format: "H:|[v0]|", views: customCollectionView)
Upvotes: 1
Views: 4496
Reputation: 77682
You can use constraints to make your cells "auto-sizing" and you won't have to do any manual calculating of the text / label sizes.
Give this a try. Of course, it will need modifications to suit your needs, but should give you something to work with (Edit: I updated my code to more closely match your approach):
//
// MenuBarViewController.swift
//
import UIKit
private let reuseIdentifier = "MyASCell"
class MyMenuCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
override var isSelected: Bool {
didSet {
labelTitre.textColor = isSelected ? UIColor.black : .lightGray
}
}
let labelTitre : UILabel = {
let label = UILabel()
label.textColor = .gray
return label
}()
func setupViews() {
backgroundColor = UIColor.white
addSubview(labelTitre)
labelTitre.translatesAutoresizingMaskIntoConstraints = false
// set constraints to use the label's intrinsic size for auto-sizing
// we'll use 10 pts for left and right padding
labelTitre.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10.0).isActive = true
labelTitre.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10.0).isActive = true
// center the label vertically (padding controlled by collectionView's layout estimated size
labelTitre.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyMenuBarViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var theLabels = [
"Intro",
"Aborigenes",
"Faune",
"Flore",
"Randonnées",
"A longer label here",
"End"
]
lazy var theCollectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 1.0
layout.minimumInteritemSpacing = 1.0
// note: since the labels are "auto-width-stretching", the height here defines the actual height of the cells
layout.estimatedItemSize = CGSize(width: 60, height: 28)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
// using lightGray for the background "fills in" the spacing, giving us "cell borders"
cv.backgroundColor = UIColor.lightGray
cv.dataSource = self
cv.delegate = self
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .yellow
// register the cell
// theCollectionView.register(MyASCell.self, forCellWithReuseIdentifier: reuseIdentifier)
theCollectionView.register(MyMenuCell.self, forCellWithReuseIdentifier: reuseIdentifier)
// we're going to add constraints, so don't use AutoresizingMask
theCollectionView.translatesAutoresizingMaskIntoConstraints = false
// add the "menu bar" to the view
self.view.addSubview(theCollectionView)
// pin collection view to left and right edges
theCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
theCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
// pin top of collection view to topLayoutGuide
theCollectionView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
// set height of collection view to 30
theCollectionView.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
// for demonstration's sake, just add a blue view below the "menu bar"
let v = UIView(frame: CGRect.zero)
v.backgroundColor = UIColor.blue
v.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(v)
// pin gray view to left, right, bottom of view
v.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
v.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
v.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
// pin top of gray view to bottom of collection view
v.topAnchor.constraint(equalTo: theCollectionView.bottomAnchor).isActive = true
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
// we want a 1-pt border on top, bottom and left and right edges of the collection view itself
return UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return theLabels.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! MyMenuCell
cell.labelTitre.text = theLabels[indexPath.row]
// just simulating selection here...
cell.isSelected = indexPath.row == 0
return cell
}
}
Edit:
Here is a cap of the result from my code:
And a cap of the result from your code - with one line edited:
In your cell code, change:
addConstraintsWithFormat(format: "H:[v0]", views: labelTitre)
to:
addConstraintsWithFormat(format: "H:|-10-[v0]-10-|", views: labelTitre)
Upvotes: 3