Search code examples
iosswiftuitableviewuikit

UITableViewCell not displaying constraints as intended


I'm trying to programatically create a tableview cell, but having some trouble getting it to layout as intended. I think its down to the order I call things but I cant get the content to display as intended (ive tried the insets and other constraints on a uiview and it seemed to work ok).

Im registering the tableview cell in the VC, Im also dequeuing a reusable cell in cellforrow where I pass the activity object to update the cell UI. The cell has a height of 300 set by the heightforrow callback.

ActivityCell code

class ActivityCell: UITableViewCell {
    
    private var activityCard = UIView()
    private var verticalStack = UIStackView()
    private var nameStack = UIStackView()      // top horizontal stack
    private let profileImage = UIImageView()
    private let profileNameLbl = UILabel()
    private let dateLbl = UILabel()
    private let raceVerticalStack = UIStackView()  //middle
    private let topRaceStack = UIStackView()
    private let bottomRaceStack = UIStackView()
    private let raceNameStack = UIStackView()
    private let raceNameLbl = UILabel()
    private let distanceStack = UIStackView()
    private let distanceLbl = UILabel()
    private let distanceValueLbl = UILabel()
    private let timeStack = UIStackView()
    private let timeLbl = UILabel()
    private let timeValueLbl = UILabel()
    private let paceStack = UIStackView()
    private let paceLbl = UILabel()
    private let paceValueLbl = UILabel()
    private let positionStack = UIStackView()
    private let positionLbl = UILabel()
    private let positionValueLbl = UILabel()
    private let pointsStack = UIStackView()
    private let pointsLbl = UILabel()
    private let pointsValueLbl = UILabel()
    private let bottomStack = UIStackView()  //bottom
    private let clapStack = UIStackView()
    private let clapIcon = UIImageView()
    private let clapCountLbl = UILabel()
    private let clapCommentStack = UIStackView()
    private let commentStack = UIStackView()
    private let commentIcon = UIImageView()
    private let commentCountLbl = UILabel()
    private let clapButton = UIButton()     // interaction buttons
    private let commentButton = UIButton()
    
    
    private let gradientStart = UIColor(red: 24/255, green: 44/255, blue: 86/255, alpha: 1.0)
    private let gradientEnd = UIColor(red: 234/255, green: 82/255, blue: 119/255, alpha: 1.0)
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        layer.masksToBounds = true
        self.contentView.layer.masksToBounds = true
        activityCard.frame = self.frame.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))
        verticalStack.frame = activityCard.frame
        
        setupHierarchy()
        setConstraints()
        configureView()
        
    }
    
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func configureView() {
        labelSetup()
        buttonSetup()
        stackViewSetup()
        imageSetup()
        stackViewSetup()
        setShadow()
    }

    func updateCell(with activity: RaceActivity) {
        profileNameLbl.text = activity.runnerName
        profileImage.image = activity.profileImage
        raceNameLbl.text = activity.raceName
        distanceValueLbl.text = activity.distance
        timeValueLbl.text = activity.time
        paceValueLbl.text = activity.pace
        positionValueLbl.text = activity.position
        pointsValueLbl.text = activity.points
    }
    
    private func labelSetup() {
        profileNameLbl.font = .Graphik()
        raceNameLbl.font = .Graphik(.medium, size: 22)
        dateLbl.font = .Graphik(.light, size: 17)
        distanceLbl.font = .Graphik()
        timeLbl.font = .Graphik()
        paceLbl.font = .Graphik()
        positionLbl.font = .Graphik()
        pointsLbl.font = .Graphik()
        clapCountLbl.font = .Graphik()
        commentCountLbl.font = .Graphik()
        paceLbl.font = .Graphik()
        distanceValueLbl.font = .Graphik(.medium)
        timeValueLbl.font = .Graphik(.medium)
        paceValueLbl.font = .Graphik(.medium)
        positionValueLbl.font = .Graphik(.medium)
   
        timeLbl.textAlignment = .left
        paceLbl.textAlignment = .center
        paceValueLbl.textAlignment = .center
        pointsLbl.textAlignment = .left
        pointsValueLbl.textAlignment = .left
        dateLbl.textAlignment = .right
        clapCountLbl.text = "33"
        commentCountLbl.text = "12"
        positionLbl.text = "Position"
        pointsLbl.text = "Points"
        distanceLbl.text = "Distance"
        timeLbl.text = "Time"
        paceLbl.text = "Pace"
        dateLbl.text = "Friday"
    }
    
    private func stackViewSetup() {
        nameStack.distribution = .fillProportionally
        topRaceStack.distribution = .fillEqually
        bottomRaceStack.distribution = .fillEqually
        clapCommentStack.distribution = .fillEqually
        bottomStack.distribution = .fillEqually
        
        raceNameStack.axis = .horizontal
        nameStack.axis = .horizontal
        verticalStack.axis = .vertical
        raceVerticalStack.axis = .vertical
        bottomStack.axis = .horizontal
        topRaceStack.axis = .horizontal
        bottomRaceStack.axis = .horizontal
        distanceStack.axis = .vertical
        timeStack.axis = .vertical
        paceStack.axis = .vertical
        positionStack.axis = .vertical
        pointsStack.axis = .vertical
        
        nameStack.spacing = 10
        verticalStack.spacing = 5
        raceVerticalStack.spacing = 10
        bottomStack.spacing = 10
        
        nameStack.alignment = .center
        distanceStack.alignment = .center
        timeStack.alignment = .center
        paceStack.alignment = .center
        pointsStack.alignment = .center
        positionStack.alignment = .center
        clapCommentStack.alignment = .center
        bottomStack.alignment = .center
        
        raceVerticalStack.spacing = 5
        topRaceStack.spacing = 2
        clapCommentStack.spacing = 3
        clapStack.spacing = 4
        commentStack.spacing = 4
    }
    
    private func buttonSetup() {
        clapButton.setTitle("Clap", for: .normal)
        clapButton.setTitleColor(.darkGray, for: .normal)
        clapButton.titleLabel?.font = .Graphik(.regular, size: 14)
        clapButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        clapButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 30)
        clapButton.setImage(UIImage(named: "clap"), for: .normal)
        commentButton.setTitle("Comment", for: .normal)
        commentButton.setTitleColor(.darkGray, for: .normal)
        commentButton.titleLabel?.font = .Graphik(.regular, size: 14)
        commentButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)
        commentButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)
        commentButton.setImage(UIImage(named: "comment"), for: .normal)
    }
    
    private func imageSetup() {
        profileImage.layer.cornerRadius = 15
        profileImage.backgroundColor = .lightGray
        clapIcon.image = UIImage(named: "clapGrey")
        clapIcon.contentMode = .scaleAspectFit
        commentIcon.image = UIImage(named: "commentGrey")
        commentIcon.contentMode = .scaleAspectFit
    }
 
    
    private func setShadow() {

        let gradient = CAGradientLayer()
        gradient.frame =  CGRect(origin: CGPoint(x: 10, y: 5), size: frame.size)
        gradient.colors = [gradientStart.cgColor, gradientEnd.cgColor]

        let border = CAShapeLayer()
        border.lineWidth = 2
        border.path = UIBezierPath(roundedRect: frame.inset(by: UIEdgeInsets(top: 10, left: 2, bottom: 10, right: 22)), cornerRadius: 12).cgPath
        border.strokeColor = UIColor.black.cgColor
        border.fillColor = UIColor.clear.cgColor
        gradient.mask = border
        removeExistingGradient(from: self)
        self.layer.addSublayer(gradient)
        
    }
    
    private func setConstraints() {
        
        let constraints = [
            nameStack.heightAnchor.constraint(equalToConstant: 50),
            raceNameStack.heightAnchor.constraint(equalToConstant: 30),
            topRaceStack.heightAnchor.constraint(equalToConstant: 60),
            bottomRaceStack.heightAnchor.constraint(equalToConstant: 60),
            profileImage.heightAnchor.constraint(equalToConstant: 30),
            profileImage.widthAnchor.constraint(equalToConstant: 30),
            commentButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 90),
            clapButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 90),
            clapIcon.widthAnchor.constraint(equalToConstant: 15),
            commentIcon.widthAnchor.constraint(equalToConstant: 15),
        ]
        NSLayoutConstraint.activate(constraints)
        setupMargins()
    }
    
    private func setupMargins() {
        nameStack.layoutMargins = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 25)
        raceNameStack.layoutMargins = UIEdgeInsets(top:0, left: 20, bottom: 0, right: 0)
        raceVerticalStack.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 35)
        bottomStack.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 10, right: 30)
        nameStack.isLayoutMarginsRelativeArrangement = true
        topRaceStack.isLayoutMarginsRelativeArrangement = true
        bottomRaceStack.isLayoutMarginsRelativeArrangement = true
        raceNameStack.isLayoutMarginsRelativeArrangement = true
        bottomStack.isLayoutMarginsRelativeArrangement = true
        addLines()
    }
    
    private func addLines() {
        guard bottomStack.frame.height > 0 else { return }
        let horizontalStart = CGPoint(x: bottomStack.frame.minX, y: bottomStack.frame.minY)
        let bottomLine = UIView(frame: CGRect(x: horizontalStart.x + 20, y: horizontalStart.y + 12, width: bottomStack.frame.width - 40, height: 1.0))
        bottomLine.tag = 99
        bottomLine.layer.borderWidth = 0.8
        bottomLine.layer.borderColor = UIColor.lightGray.cgColor

        let clapStackLine = UIView(frame: CGRect(x: clapCommentStack.frame.maxX, y: horizontalStart.y + 22, width: 1.0, height: clapCommentStack.frame.maxY - clapCommentStack.frame.minY))
        clapStackLine.layer.borderWidth = 0.8
        clapStackLine.layer.borderColor = UIColor.lightGray.cgColor
        let commentLine = UIView(frame: CGRect(x: clapButton.frame.maxX, y: horizontalStart.y + 22, width: 1.0, height: clapCommentStack.frame.maxY - clapCommentStack.frame.minY))
        commentLine.layer.borderWidth = 0.8
        commentLine.layer.borderColor = UIColor.lightGray.cgColor
        let topLine = UIView(frame: CGRect(x: horizontalStart.x + 20, y: nameStack.frame.maxY + 5, width: bottomStack.frame.width - 40, height: 1.0))
        topLine.layer.borderWidth = 0.8
        topLine.layer.borderColor = UIColor.lightGray.cgColor
        self.activityCard.addSubview(bottomLine)
        self.activityCard.addSubview(clapStackLine)
        self.activityCard.addSubview(commentLine)
        self.activityCard.addSubview(topLine)
    }
    
    
    private func setupHierarchy() {
        nameStack.addArrangedSubview(profileImage)
        nameStack.addArrangedSubview(profileNameLbl)
        nameStack.addArrangedSubview(dateLbl)
        
        raceVerticalStack.addArrangedSubview(raceNameStack)
        raceVerticalStack.addArrangedSubview(topRaceStack)
        raceVerticalStack.addArrangedSubview(bottomRaceStack)
        
        raceNameStack.addArrangedSubview(raceNameLbl)
        
        topRaceStack.addArrangedSubview(distanceStack)
        topRaceStack.addArrangedSubview(timeStack)
        topRaceStack.addArrangedSubview(paceStack)
        
        bottomRaceStack.addArrangedSubview(positionStack)
        bottomRaceStack.addArrangedSubview(pointsStack)
        bottomRaceStack.addArrangedSubview(UIView())
        
        distanceStack.addArrangedSubview(distanceLbl)
        distanceStack.addArrangedSubview(distanceValueLbl)
        timeStack.addArrangedSubview(timeLbl)
        timeStack.addArrangedSubview(timeValueLbl)
        paceStack.addArrangedSubview(paceLbl)
        paceStack.addArrangedSubview(paceValueLbl)
        positionStack.addArrangedSubview(positionLbl)
        positionStack.addArrangedSubview(positionValueLbl)
        pointsStack.addArrangedSubview(pointsLbl)
        pointsStack.addArrangedSubview(pointsValueLbl)
        
        clapStack.addArrangedSubview(clapIcon)
        clapStack.addArrangedSubview(clapCountLbl)
        
        commentStack.addArrangedSubview(commentIcon)
        commentStack.addArrangedSubview(commentCountLbl)
        
        clapCommentStack.addArrangedSubview(clapStack)
        clapCommentStack.addArrangedSubview(commentStack)
        
        mainStackHierarchy()
    }
    
    private func mainStackHierarchy() {
        bottomStack.addArrangedSubview(clapCommentStack)
        bottomStack.addArrangedSubview(clapButton)
        bottomStack.addArrangedSubview(commentButton)
        
        verticalStack.addArrangedSubview(nameStack)
        verticalStack.addArrangedSubview(raceNameStack)
        verticalStack.addArrangedSubview(raceVerticalStack)
        verticalStack.addArrangedSubview(bottomStack)
        activityCard.addSubview(verticalStack)
        self.contentView.addSubview(activityCard)
    }
}

screenshot of current output:


screenshot of intended output:


Any help is appreciated :D


Solution

  • You're having problems because you're using a lot of explicit frames instead of taking advantage of auto-layout.

    First, if you change your init() func to this and run your app:

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        layer.masksToBounds = true
        self.contentView.layer.masksToBounds = true
        
        // no need to set frames
        //activityCard.frame = self.frame.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))
        //verticalStack.frame = activityCard.frame
        
        setupHierarchy()
        setConstraints()
        configureView()
    
        activityCard.translatesAutoresizingMaskIntoConstraints = false
        verticalStack.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            
            activityCard.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10.0),
            activityCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10.0),
            activityCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10.0),
            activityCard.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10.0),
    
            verticalStack.topAnchor.constraint(equalTo: activityCard.topAnchor, constant: 6.0),
            verticalStack.leadingAnchor.constraint(equalTo: activityCard.leadingAnchor, constant: 6.0),
            verticalStack.trailingAnchor.constraint(equalTo: activityCard.trailingAnchor, constant: -6.0),
            verticalStack.bottomAnchor.constraint(equalTo: activityCard.bottomAnchor, constant: -6.0),
    
        ])
        
    }
    

    You'll see that you are much closer to what you want.

    Next, change:

    private var activityCard = UIView()
    

    to:

    private var activityCard = ActivityCardView() 
    

    and use this class:

    class ActivityCardView: UIView {
        private let gradientStart = UIColor(red: 24/255, green: 44/255, blue: 86/255, alpha: 1.0)
        private let gradientEnd = UIColor(red: 234/255, green: 82/255, blue: 119/255, alpha: 1.0)
    
        private let gradLayer = CAGradientLayer()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            layer.addSublayer(gradLayer)
            gradLayer.colors = [gradientStart.cgColor, gradientEnd.cgColor]
        }
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        override func layoutSubviews() {
            super.layoutSubviews()
    
            gradLayer.frame = bounds
            
            let border = CAShapeLayer()
            border.lineWidth = 2
            border.path = UIBezierPath(roundedRect: bounds.inset(by: UIEdgeInsets(top: 10, left: 2, bottom: 10, right: 2)), cornerRadius: 12).cgPath
            border.strokeColor = UIColor.black.cgColor
            border.fillColor = UIColor.clear.cgColor
            gradLayer.mask = border
        }
    }
    

    then, remove your private func setShadow() from the cell class. This ActivityCardView class will automatically handle the gradient border.

    For your "separator" lines, you're probably much better off adding them as arranged subviews of your verticalStack than trying to position them with absolute coordinates:

    private func mainStackHierarchy() {
        bottomStack.addArrangedSubview(clapCommentStack)
        bottomStack.addArrangedSubview(clapButton)
        bottomStack.addArrangedSubview(commentButton)
        
        verticalStack.addArrangedSubview(nameStack)
        
        var hLineView = UIView()
        hLineView.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
        hLineView.backgroundColor = .lightGray
        verticalStack.addArrangedSubview(hLineView)
        
        verticalStack.addArrangedSubview(raceNameStack)
        verticalStack.addArrangedSubview(raceVerticalStack)
    
        hLineView = UIView()
        hLineView.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
        hLineView.backgroundColor = .lightGray
        verticalStack.addArrangedSubview(hLineView)
        
        verticalStack.addArrangedSubview(bottomStack)
        activityCard.addSubview(verticalStack)
        self.contentView.addSubview(activityCard)
    }
    

    At this point, your output should look about like this:

    enter image description here

    The underline on the race name would probably best be handled by using attributed text with underline style.

    I assume you'll want to make some adjustments to spacing -- but that shouldn't be any problem.