Search code examples
iosswiftxcodeuitableviewuistackview

Multiple UIStackViews inside a custom UITableViewCell in Custom Cell without Storyboard not working


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!


Solution

  • 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