Home > Back-end >  Multiple UIStackViews inside a custom UITableViewCell in Custom Cell without Storyboard not working
Multiple UIStackViews inside a custom UITableViewCell in Custom Cell without Storyboard not working

Time:11-12

I am currently building out a screen in my app which is basically a long UITableView containing 3 Sections, each with different amounts of unique custom cells. Setting up The tableview works fine, I added some random text in the cells to make sure every cell is correctly called and positioned. I have completely deletet my storyboard from my project because it would lead to problems later because of reasons. So I can't do anything via storyboard.

Next step is to build the custom cells. Some of those are fairly complex for me as a beginner. This is one of my cells:Example of one of my cells

I want to split the cell in multiple UIStackViews, one for the picture and the name and one for the stats on the right side which in itself will contain two stackviews containing each of the two rows of stats. Each of these could then contain another embedded stackview with the two uiLabels inside, the number and the description. Above all that is a toggle button.

I can't seem to grasp how to define all this. As I said, I defined the Tableview and am calling the right cells in my cellForRowAt as shown here for example:

if indexPath.row == 0 && indexPath.section == 0 {
        let cell = tableView.dequeueReusableCell(withIdentifier: StatsOverViewCell.identifier, for: indexPath) as! StatsOverViewCell
        cell.configure()
        return cell
} else if ...

I have created files for each cell, one of them being StatsOverViewCell. In this file, I have an Identifier with the same name as the class. I have also added the configure function I am calling from my tableview, the layoutSubviews function which I use to layout the views inside of the cell and I have initialized every label and image I need. I have trimmed the file down to a few examples to save you some time:

class StatsOverViewCell: UITableViewCell {

//Set identifier to be able to call it later on
static let identifier = "StatsOverViewCell"

let myProfileStackView = UIStackView()
let myImageView = UIImageView()
let myName = UILabel()
let myGenderAndAge = UILabel()

let myStatsStackView = UIStackView()

let oneView = UIStackView()
let oneStat = UILabel()
let oneLabel = UILabel()

let twoStackView = UIStackView()
let twoStat = UILabel()
let twoLabel = UILabel()

//Do this for each of the labels I have in the stats

public func configure() {
    myImageView.image = UIImage(named: "person-icon")
    myName.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
    myImageView.contentMode = .scaleAspectFill
    myName.text = "Name."
    myName.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
    myName.textAlignment = .center
    //Add the Name label to the stackview
    myProfileStackView.addArrangedSubview(myName)
    myProfileStackView.addArrangedSubview(myImageView)
    myName.centerXAnchor.constraint(equalTo: myProfileStackView.centerXAnchor).isActive = true
    
    oneStat.text = "5.187"
    oneStat.font = UIFont(name: "montserrat", size: 18)
    oneLabel.text = "Text"
    oneLabel.font = UIFont(name: "montserrat", size: 14)
}

//Layout in the cell
override func layoutSubviews() {
    super.layoutSubviews()
    contentView.backgroundColor = Utilities.hexStringToUIColor(hex: "#3F454B")
    contentView.layer.borderWidth = 1
    
    //Stackview
    contentView.addSubview(myProfileStackView)
    myProfileStackView.axis = .vertical
    myProfileStackView.distribution = .equalSpacing
    myProfileStackView.spacing = 3.5
    myProfileStackView.backgroundColor = .red
    
    myProfileStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 23).isActive = true
    myProfileStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 76).isActive = true
}

As you can see, I am adding all arrangedsubviews to the stackview in the configure method which I call when creating the cell in the tableview. I then set the stackviews constraints inside the layoutsubviews. I am not getting any errors or anything. But the cell shows up completely empty.

I feel like I am forgetting something or I am not understanding how to create cells with uistackviews. Where should I create the stackviews, where should I add the arrangedsubviews to this stackview and what do I do in the LayoutSubviews? I would be very thankful for any insights. Thanks for your time!

CodePudding user response:

You're doing a few things wrong...

  1. your UI elements should be created and configured in init, not in configure() or layoutSubviews()
  2. you need complete constraints to give your elements the proper layout

Take a look at the changes I made to your cell class. It should get you on your way:

class StatsOverViewCell: UITableViewCell {
    
    //Set identifier to be able to call it later on
    static let identifier = "StatsOverViewCell"
    
    let myProfileStackView = UIStackView()
    let myImageView = UIImageView()
    let myName = UILabel()
    let myGenderAndAge = UILabel()
    
    let myStatsStackView = UIStackView()
    
    let oneView = UIStackView()
    let oneStat = UILabel()
    let oneLabel = UILabel()
    
    let twoStackView = UIStackView()
    let twoStat = UILabel()
    let twoLabel = UILabel()
    
    //Do this for each of the labels I have in the stats
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    func commonInit() {
        myImageView.image = UIImage(named: "person-icon")

        // frame doesn't matter - stack view arrangedSubvies automatically
        //  set .translatesAutoresizingMaskIntoConstraints = false
        //myName.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        
        myImageView.contentMode = .scaleAspectFill
        myName.text = "Name."
        
        myName.textAlignment = .center
        
        //Add the Name label to the stackview
        myProfileStackView.addArrangedSubview(myName)
        myProfileStackView.addArrangedSubview(myImageView)
        
        // no need for this
        //myName.centerXAnchor.constraint(equalTo: myProfileStackView.centerXAnchor).isActive = true
        
        oneStat.text = "5.187"
        oneStat.font = UIFont(name: "montserrat", size: 18)
        oneLabel.text = "Text"
        oneLabel.font = UIFont(name: "montserrat", size: 14)
        
        contentView.backgroundColor = Utilities.hexStringToUIColor(hex: "#3F454B")
        contentView.layer.borderWidth = 1
        
        //Stackview
        contentView.addSubview(myProfileStackView)
        myProfileStackView.axis = .vertical
        
        // no need for equalSpacing if you're explicitly setting the spacing
        //myProfileStackView.distribution = .equalSpacing
        myProfileStackView.spacing = 3.5
        myProfileStackView.backgroundColor = .red
        
        // stack view needs .translatesAutoresizingMaskIntoConstraints = false
        myProfileStackView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([

            // stack view leading 23-pts from contentView leading
            myProfileStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 23),

            // stack view top 76-pts from contentView top
            myProfileStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 76),
            
            // need something to set the contentView height
            
            // stack view bottom 8-pts from contentView bottom
            myProfileStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
            
            // set imageView width and height
            myImageView.widthAnchor.constraint(equalToConstant: 100.0),
            myImageView.heightAnchor.constraint(equalTo: myImageView.widthAnchor),

        ])
    }
    
    public func configure() {
        // here you would set the properties of your elements, such as
        //  label text
        //  imageView image
        //  colors
        //  etc
    }
}

Edit

Here's an example cell class that comes close to the layout in the image you posted.

Note that there are very few constraints needed:

NSLayoutConstraint.activate([
        
    // role element 12-pts from top
    myRoleElement.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12.0),
    // centered horizontally
    myRoleElement.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
    // it will probably be using intrinsic height and width, but for demo purposes
    myRoleElement.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.4),
    myRoleElement.heightAnchor.constraint(equalToConstant: 40.0),
        
    // stack view 24-pts on each side
    hStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
    hStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
    // stack view 20-pts on bottom
    hStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20),
    // stack view top 20-pts from Role element bottom
    hStack.topAnchor.constraint(equalTo: myRoleElement.bottomAnchor, constant: 20),
        
    // set imageView width and height
    myImageView.widthAnchor.constraint(equalToConstant: 100.0),
    myImageView.heightAnchor.constraint(equalTo: myImageView.widthAnchor),
        
    // we want the two "column" stack views to be equal widths
    hStack.arrangedSubviews[1].widthAnchor.constraint(equalTo: hStack.arrangedSubviews[2].widthAnchor),
        
])

Here's the full cell class, including an example "UserStruct" ... you will, of course, want to tweak the fonts / sizes, spacing, etc:

// sample struct for user data
struct UserStruct {
    var profilePicName: String = ""
    var name: String = ""
    var gender: String = ""
    var age: Int = 0
    var statValues: [String] = []
}

class StatsOverViewCell: UITableViewCell {
    
    //Set identifier to be able to call it later on
    static let identifier = "StatsOverViewCell"
    
    // whatever your "role" element is...
    let myRoleElement = UILabel()
    
    let myImageView = UIImageView()
    let myName = UILabel()
    let myGenderAndAge = UILabel()

    var statValueLabels: [UILabel] = []

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    func commonInit() {
        
        // create 6 Value and 6 text labels
        // assuming you have 6 "Text" strings, but for now
        //  we'll use "Text A", "Text B", etc
        let tmp: [String] = [
            "A", "B", "C",
            "D", "E", "F",
        ]

        var statTextLabels: [UILabel] = []

        for i in 0..<6 {

            var lb = UILabel()
            lb.font = UIFont.systemFont(ofSize: 16, weight: .regular)
            lb.textAlignment = .center
            lb.textColor = .white
            lb.text = "0"
            statValueLabels.append(lb)
            
            lb = UILabel()
            lb.font = UIFont.systemFont(ofSize: 13, weight: .regular)
            lb.textAlignment = .center
            lb.textColor = .lightGray
            lb.text = "Text \(tmp[i])"
            statTextLabels.append(lb)
            
        }
        
        // name and Gender/Age label properties
        myName.textAlignment = .center
        myGenderAndAge.textAlignment = .center
        myName.font = UIFont.systemFont(ofSize: 15, weight: .regular)
        myGenderAndAge.font = UIFont.systemFont(ofSize: 15, weight: .regular)
        myName.textColor = .white
        myGenderAndAge.textColor = .white

        // placeholder text
        myName.text = "Name"
        myGenderAndAge.text = "(F, 26)"
        
        myImageView.contentMode = .scaleAspectFill
        
        // create the "Profile" stack view
        let myProfileStackView = UIStackView()
        myProfileStackView.axis = .vertical
        myProfileStackView.spacing = 2

        //Add imageView, name and gender/age labels to the profile stackview
        myProfileStackView.addArrangedSubview(myImageView)
        myProfileStackView.addArrangedSubview(myName)
        myProfileStackView.addArrangedSubview(myGenderAndAge)
        
        // create horizontal stack view to hold
        //  Profile stack   two "column" stack views
        let hStack = UIStackView()

        // add Profile stack view
        hStack.addArrangedSubview(myProfileStackView)
        
        var j: Int = 0

        // create two "column" stack views
        //  each with three "label pair" stack views
        for _ in 0..<2 {
            let columnStack = UIStackView()
            columnStack.axis = .vertical
            columnStack.distribution = .equalSpacing

            for _ in 0..<3 {
                let pairStack = UIStackView()
                pairStack.axis = .vertical
                pairStack.spacing = 4
                pairStack.addArrangedSubview(statValueLabels[j])
                pairStack.addArrangedSubview(statTextLabels[j])
                columnStack.addArrangedSubview(pairStack)
                j  = 1
            }
            
            hStack.addArrangedSubview(columnStack)
        }
        
        // whatever your "Roles" element is...
        //  here, we'll simulate it with a label
        myRoleElement.text = "Role 1 / Role 2"
        myRoleElement.textAlignment = .center
        myRoleElement.textColor = .white
        myRoleElement.backgroundColor = .systemTeal
        myRoleElement.layer.cornerRadius = 8
        myRoleElement.layer.borderWidth = 1
        myRoleElement.layer.borderColor = UIColor.white.cgColor
        myRoleElement.layer.masksToBounds = true
        
        // add Role element and horizontal stack view to contentView
        contentView.addSubview(myRoleElement)
        contentView.addSubview(hStack)
        
        myRoleElement.translatesAutoresizingMaskIntoConstraints = false
        hStack.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            
            // role element 12-pts from top
            myRoleElement.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12.0),
            // centered horizontally
            myRoleElement.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            // it will probably be using intrinsic height and width, but for demo purposes
            myRoleElement.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.4),
            myRoleElement.heightAnchor.constraint(equalToConstant: 40.0),
            
            // stack view 24-pts on each side
            hStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
            hStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
            // stack view 20-pts on bottom
            hStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20),
            // stack view top 20-pts from Role element bottom
            hStack.topAnchor.constraint(equalTo: myRoleElement.bottomAnchor, constant: 20),
            
            // set imageView width and height
            myImageView.widthAnchor.constraint(equalToConstant: 100.0),
            myImageView.heightAnchor.constraint(equalTo: myImageView.widthAnchor),
            
            // we want the two "column" stack views to be equal widths
            hStack.arrangedSubviews[1].widthAnchor.constraint(equalTo: hStack.arrangedSubviews[2].widthAnchor),
            
        ])

        //contentView.backgroundColor = Utilities.hexStringToUIColor(hex: "#3F454B")
        contentView.backgroundColor = UIColor(red: 0x3f / 255.0, green: 0x45 / 255.0, blue: 0x4b / 255.0, alpha: 1.0)
        contentView.layer.borderWidth = 1
        contentView.layer.borderColor = UIColor.lightGray.cgColor
        
        // since we're setting the image view to explicit 100x100 size,
        //  we can make it round here
        myImageView.layer.cornerRadius = 50
        myImageView.layer.masksToBounds = true
    }
    
    public func configure(_ user: UserStruct) {
        // here you would set the properties of your elements

        // however you're getting your profile image
        var img: UIImage!
        if !user.profilePicName.isEmpty {
            img = UIImage(named: user.profilePicName)
        }
        if img == nil {
            img = UIImage(named: "person-icon")
        }
        if img != nil {
            myImageView.image = img
        }
        
        myName.text = user.name
        myGenderAndAge.text = "(\(user.gender), \(user.age))"
        
        // probably want error checking to make sure we have 6 values
        if user.statValues.count == 6 {
            for (lbl, s) in zip(statValueLabels, user.statValues) {
                lbl.text = s
            }
        }
    }
    
}

and a sample table view controller:

class UserStatsTableViewController: UITableViewController {
    
    var myData: [UserStruct] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.register(StatsOverViewCell.self, forCellReuseIdentifier: StatsOverViewCell.identifier)
        
        // generate some sample data
        //      I'm using Female "pro1" and Male "pro2" images
        for i in 0..<10 {
            var user = UserStruct(profilePicName: i % 2 == 0 ? "pro2" : "pro1",
                                  name: "Name \(i)",
                                  gender: i % 2 == 0 ? "F" : "M",
                                  age: Int.random(in: 21...65))
            var vals: [String] = []
            for _ in 0..<6 {
                let v = Int.random(in: 100..<1000)
                vals.append("\(v)")
            }
            user.statValues = vals
            myData.append(user)
        }
    }
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: StatsOverViewCell.identifier, for: indexPath) as! StatsOverViewCell
        let user = myData[indexPath.row]
        cell.configure(user)
        return cell
    }
}

This is how it looks at run-time:

enter image description here

  • Related