1

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 : enter image description here

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)
petaire
  • 495
  • 1
  • 10
  • 23
  • Could this other [question about resizing cells to fit their content](https://stackoverflow.com/questions/26184098/adjust-uicollectionviewcell-to-fit-content) help? – Jack G. Jun 18 '17 at 15:47
  • It could, if I was using Interface Builder, but I'm not… – petaire Jun 18 '17 at 16:27
  • A shame, because creating a UI for the different screen sizes and resolutions is quite productive with Interface Builder nowadays vs having to code and run the application to see how it renders after every code modification. – Jack G. Jun 18 '17 at 16:32
  • I'm not going to debate the utility of IB in here, it's not really the point, but I couldn't disagree more. I've started that app with IB and it was a huge mess, with outlets connecting things everywhere, adding lines and complexity. – petaire Jun 18 '17 at 16:34
  • What exactly do you want? You want to make your labels multiline to avoid long cells? Sorry couldn't get what you are looking for. – PGDev Jun 19 '17 at 12:57

1 Answers1

3

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:

enter image description here

And a cap of the result from your code - with one line edited:

enter image description here

In your cell code, change:

addConstraintsWithFormat(format: "H:[v0]", views: labelTitre)

to:

addConstraintsWithFormat(format: "H:|-10-[v0]-10-|", views: labelTitre)
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • I was quite close of what you suggest, but I don't get a good result as you can see over there : https://tof.cx/image/FMJRG I also add some more code in the original post. – petaire Jun 18 '17 at 20:09
  • Yes, your code is close... see the edit to my answer. – DonMag Jun 19 '17 at 13:31