[SOLVED] iOS Swift – CollectionView or TableView with fixed height that expands horizontally .. Any Idea?

Issue

Hello dear developers!

I hope somebody can help me.

I’m looking for a way to create a TableView (i.e. a list with dynamic Sections and Rows) with a fixed height but dynamic width..

Example:
The TableView (or CollectionView) has a height of 600px, let’s say 10 rows fit into the height. The 11th row should now not be below, but continue to the right..

Below is a picture.

I really hope that someone can help me.. thank you in advance for reading.

PS: I invite the one with a solution for a coffee :b

->>> I think the picture shows clearly what I mean <<<-

Solution

This could be done pretty straight-forward as a UICollectionView with a horizontal flow layout.

  • Create one cell layout as the "Header" cell.
  • Create a second cell layout as the "Detail" cell.
  • arrange your data to be a single section, with data elements identified as "header" or "detail"

Quick example:

enum MyCellType: Int {
    case header
    case detail
}
struct MyStruct {
    var type: MyCellType = .header
    var headerString: String = ""
    var detailString: String = ""
    var detailValue: String = ""
}
class TestViewController: UIViewController {
    var myData: [MyStruct] = []
    
    var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBlue
        
        // some sample data
        let headers: [String] = [
            "SOFTWARE", "BATTERY", "DEVICE", "HARDWARE", "MLB", "USAGE",
        ]
        let counts: [Int] = [
            4, 2, 7, 5, 1, 8,
        ]
        var val: Int = 1
        for (i, s) in headers.enumerated() {
            let t: MyStruct = MyStruct(type: .header, headerString: s, detailString: "", detailValue: "")
            myData.append(t)
            for j in 1...counts[i % counts.count] {
                let t: MyStruct = MyStruct(type: .detail, headerString: "", detailString: "Detail \(i),\(j)", detailValue: "Val: \(val)")
                myData.append(t)
                val += 1
            }
        }
    
        let fl = UICollectionViewFlowLayout()
        fl.scrollDirection = .horizontal
        fl.minimumLineSpacing = 12
        fl.minimumInteritemSpacing = 0
        fl.itemSize = CGSize(width: 200, height: 30)
        
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(collectionView)
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            collectionView.heightAnchor.constraint(equalToConstant: 300.0),
        ])
        
        collectionView.register(MyHeaderCell.self, forCellWithReuseIdentifier: "headerCell")
        collectionView.register(MyDetailCell.self, forCellWithReuseIdentifier: "detailCell")
        
        collectionView.dataSource = self
        collectionView.delegate = self
    }
    
}

extension TestViewController: UICollectionViewDataSource, UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let t = myData[indexPath.item]
        if t.type == .header {
            let c = collectionView.dequeueReusableCell(withReuseIdentifier: "headerCell", for: indexPath) as! MyHeaderCell
            c.label.text = t.headerString
            return c
        }
        let c = collectionView.dequeueReusableCell(withReuseIdentifier: "detailCell", for: indexPath) as! MyDetailCell
        c.detailLabel.text = t.detailString
        c.valueLabel.text = t.detailValue
        // we don't want to show the "separator line" if the next
        //  item is a "header"
        if indexPath.item < myData.count - 1 {
            c.hideSepLine(myData[indexPath.item + 1].type == .header)
        }
        return c
    }
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return myData.count
    }
}

class MyHeaderCell: UICollectionViewCell {
    
    let label = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .systemFont(ofSize: 10, weight: .bold)
        contentView.addSubview(label)
        contentView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            label.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
        ])
    }
}

class MyDetailCell: UICollectionViewCell {
    
    let detailLabel = UILabel()
    let valueLabel = UILabel()
    let sepLineView = UIView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        
        detailLabel.font = .systemFont(ofSize: 12.0, weight: .light)
        valueLabel.font = .systemFont(ofSize: 12.0, weight: .light)
        valueLabel.textColor = .gray

        sepLineView.backgroundColor = .lightGray
        
        [detailLabel, valueLabel, sepLineView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(v)
        }

        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([

            detailLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
            detailLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),

            valueLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            valueLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),

            sepLineView.leadingAnchor.constraint(equalTo: detailLabel.leadingAnchor),
            sepLineView.trailingAnchor.constraint(equalTo: valueLabel.trailingAnchor),
            sepLineView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            sepLineView.heightAnchor.constraint(equalToConstant: 1.0),

        ])
        
    }
    
    func hideSepLine(_ b: Bool) {
        sepLineView.isHidden = b
    }
}

Results:

enter image description here

scrolled a little to the left:

enter image description here

scrolled all the way to the left:

enter image description here

Answered By – DonMag

Answer Checked By – Mary Flores (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *