How do I fix the Constraints Spacing?

Viewed 47

I am trying to create the view porgrammatically as show in image below. However the whole thing scatter once I embed the three horizontal stack view in the vertical stack view . I have set the spacing and alignment but for some reason it’s not showing the views properly when i run it in playgroung; the spacing gets messy. How do I resolve this issue ? Please ignore the pencil image. Its just there as a placeholder

import UIKit
import SnapKit

import Foundation
import PlaygroundSupport

extension UIStackView {
   
  func removeAllSubviews() {
    arrangedSubviews.forEach { $0.removeFromSuperview() }
  }
   
  convenience init(frame: CGRect = .zero,
           alignment: UIStackView.Alignment = .fill,
           axis: NSLayoutConstraint.Axis = .vertical,
           distribution: UIStackView.Distribution = .equalSpacing,
           spacing: CGFloat = 0.0) {
     
    self.init(frame: frame)
     
    self.alignment = alignment
    self.axis = axis
    self.distribution = distribution
    self.spacing = spacing
  }
}

class UserCell: UIView {
   
  private lazy var reputationLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = UIFont.systemFont(ofSize: 15, weight: .bold)
    label.text = "Reputation"
    return label
  }()
   
  private lazy var increasePerformaceLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = UIFont.systemFont(ofSize: 10, weight: .semibold)
    label.text = "Increase Performance"
    return label
  }()
   
   
  private lazy var increasePerformaceImage : UIImageView = {
    let imageView = UIImageView()
    imageView.translatesAutoresizingMaskIntoConstraints = false
    imageView.image = UIImage(systemName: "pencil.circle.fill")
    return imageView
  }()
   
  private lazy var reviewsImage : UIImageView = {
    let imageView = UIImageView()
    imageView.translatesAutoresizingMaskIntoConstraints = false
    imageView.image = UIImage(systemName: "pencil.circle.fill")
    return imageView
  }()
   
  private lazy var reviewsLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = UIFont.systemFont(ofSize: 13, weight: .bold)
    label.text = "Reviews"
    return label
     
  }()
  private lazy var dateLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = UIFont.systemFont(ofSize: 13, weight: .bold)
    label.text = "All Time as of 2020-10-10"
    return label
     
  }()
   
  private lazy var scoreLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = UIFont.systemFont(ofSize: 15, weight: .semibold)
    label.textColor = .darkGray
    label.text = "325"
    return label
  }()
   
  private lazy var ratingsLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
    label.textColor = .darkGray
    label.text = "Ratings"
    return label
  }()
   
   
  private lazy var ratingsNumberLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
    label.textColor = .darkGray
    label.text = "4.5"
    return label
  }()
   
  private lazy var ratingStarImage : UIImageView = {
    let imageView = UIImageView()
    imageView.translatesAutoresizingMaskIntoConstraints = false
    imageView.image = UIImage(systemName: "pencil.circle.fill")
    return imageView
  }()
   
   
  override init(frame: CGRect) {
    super.init(frame: frame)
    addComponents()
  }
   
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
     
  }
   
   
  private func addComponents() {
     
     
    let reputationStackView = UIStackView(frame: .zero, alignment: .top, axis: .horizontal, distribution: .equalCentering, spacing:25)
     
    let reviewStackView = UIStackView(frame: .zero, alignment: .top, axis: .horizontal, distribution: .equalCentering, spacing: 25)
     
     
    let ratingStackView = UIStackView(frame: .zero, alignment: .top, axis: .horizontal, distribution: .equalSpacing, spacing: 20)
     
    let dateStackView = UIStackView(frame: .zero, alignment: .top, axis: .horizontal, distribution: .equalSpacing, spacing: 20)
     
     
    let verticalStackView = UIStackView(frame: .zero, alignment: .top, axis: .vertical, distribution: .fill, spacing: 9)
     
     
    reputationStackView.addArrangedSubview(reputationLabel)
    reputationStackView.addArrangedSubview(increasePerformaceLabel)
    reputationStackView.addArrangedSubview(increasePerformaceImage)
    
    reviewStackView.addArrangedSubview(reviewsImage)
    reviewStackView.addArrangedSubview(reviewsLabel)
    reviewStackView.addArrangedSubview(scoreLabel)
     
    dateStackView.addArrangedSubview(dateLabel)

     
    ratingStackView.addArrangedSubview(ratingsLabel)
    ratingStackView.addArrangedSubview(ratingsNumberLabel)
    ratingStackView.addArrangedSubview(ratingStarImage)
     
    verticalStackView.addArrangedSubview(reputationStackView)
    verticalStackView.addArrangedSubview(reviewStackView)
    verticalStackView.addArrangedSubview(dateStackView)
    verticalStackView.addArrangedSubview(ratingStackView)
     
    addSubview(verticalStackView)
     
    increasePerformaceImage.snp.makeConstraints { (make) in
      make.width.height.equalTo(20)
    }
    reviewsImage.snp.makeConstraints { (make) in
      make.width.height.equalTo(20)
    }
    ratingStarImage.snp.makeConstraints { (make) in
      make.width.height.equalTo(20)
    }
     
    verticalStackView.snp.makeConstraints { (make) in
          make.edges.equalToSuperview()
        }
  }
}


let userCell = UserCell(frame: CGRect(x: 0, y: 0, width: 400, height: 100))
PlaygroundPage.current.liveView = userCell
1 Answers

As I mentioned in my comment... during layout development it can be a great help to give UI elements contrasting background colors to make it easy to see their frames at run-time.

Also, it really, really, REALLY helps to add comments in your code, so you know what you expect to happen.

Take a look at this modification to your UserCell class:

class UserCell: UIView {
    
    private lazy var reputationLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 15, weight: .bold)
        label.text = "Reputation"
        return label
    }()
    
    private lazy var increasePerformaceLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 10, weight: .semibold)
        label.textColor = .gray
        label.text = "Increased Performance"
        return label
    }()
    
    private lazy var increasePerformaceImage : UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(systemName: "pencil.circle.fill")
        return imageView
    }()
    
    private lazy var reviewsImage : UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(systemName: "pencil.circle.fill")
        return imageView
    }()
    
    private lazy var reviewsLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 13, weight: .bold)
        label.text = "Reviews"
        return label
    }()
    
    private lazy var dateLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 13, weight: .bold)
        label.textColor = .gray
        label.text = "All Time as of 2020-10-10"
        return label
    }()
    
    private lazy var scoreLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 28, weight: .semibold)
        label.textColor = .darkGray
        label.text = "325"
        return label
    }()
    
    private lazy var ratingsLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
        label.textColor = .darkGray
        label.text = "Rating"
        return label
    }()
    
    private lazy var ratingsNumberLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
        label.textColor = .darkGray
        label.text = "4.5"
        return label
    }()
    
    private var colorBackgrounds: Bool = false
    convenience init(debug: Bool) {
        self.init(frame: .zero)
        self.colorBackgrounds = debug
        addComponents()
    }
    
    private func addComponents() {
        
        // horizontal stack view for the "top row" - alignment is Vertical Centering
        let repStack = UIStackView(frame: .zero, alignment: .center, axis: .horizontal, distribution: .fill, spacing: 8.0)
        repStack.addArrangedSubview(reputationLabel)
        repStack.addArrangedSubview(increasePerformaceLabel)
        repStack.addArrangedSubview(increasePerformaceImage)
        
        // increasePerformaceLabel needs right alignment
        increasePerformaceLabel.textAlignment = .right

        // increasePerformaceImage 20x20
        increasePerformaceImage.snp.makeConstraints { (make) in
            make.width.height.equalTo(20.0)
        }

        // horizontal stack view for the "middle row" - alignment is Vertical Centering
        let revStack = UIStackView(frame: .zero, alignment: .center, axis: .horizontal, distribution: .fill, spacing: 8.0)
        // we'll use a vertical "sub stack" for the left-side of this "row" - alignment is Leading
        let revLeftStack = UIStackView(frame: .zero, alignment: .leading, axis: .vertical, distribution: .fill, spacing: 2.0)
        // we'll use a horizontal "sub stack" for the "top line" of the "left side" - alignment is Vertical Centering
        let revLeftTopStack = UIStackView(frame: .zero, alignment: .center, axis: .horizontal, distribution: .fill, spacing: 4.0)
        
        revLeftTopStack.addArrangedSubview(reviewsImage)
        revLeftTopStack.addArrangedSubview(reviewsLabel)
        
        revLeftStack.addArrangedSubview(revLeftTopStack)
        revLeftStack.addArrangedSubview(dateLabel)
        
        revStack.addArrangedSubview(revLeftStack)
        revStack.addArrangedSubview(scoreLabel)
        
        // reviewsImage 20x20
        reviewsImage.snp.makeConstraints { (make) in
            make.width.height.equalTo(20.0)
        }
        
        // horizontal stack view for the "bottom row" - alignment is Vertical Centering
        let ratStack = UIStackView(frame: .zero, alignment: .center, axis: .horizontal, distribution: .fill, spacing: 8.0)
        // horizontal stack view for the stars - alignment is Vertical Centering
        let starStack = UIStackView(frame: .zero, alignment: .center, axis: .horizontal, distribution: .fill, spacing: 2.0)

        ratStack.addArrangedSubview(ratingsLabel)
        ratStack.addArrangedSubview(ratingsNumberLabel)
        ratStack.addArrangedSubview(starStack)
        
        // just for example, 4 filled stars and 1 half-filled star
        for _ in 1...4 {
            if let img = UIImage(systemName: "star.fill") {
                let v = UIImageView(image: img)
                v.snp.makeConstraints { (make) in
                    make.height.equalTo(16.0)
                    make.width.equalTo(v.snp.height).multipliedBy(1.2)
                }
                starStack.addArrangedSubview(v)
            }
        }
        for _ in 5...5 {
            if let img = UIImage(systemName: "star.lefthalf.fill") {
                let v = UIImageView(image: img)
                v.snp.makeConstraints { (make) in
                    make.height.equalTo(16.0)
                    make.width.equalTo(v.snp.height).multipliedBy(1.2)
                }
                starStack.addArrangedSubview(v)
            }
        }

        // we want two "separator" lines
        let sepLineViewA = UIView()
        sepLineViewA.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        let sepLineViewB = UIView()
        sepLineViewB.backgroundColor = sepLineViewA.backgroundColor

        // vertical stack view to hold the "rows" and sep lines
        let vs = UIStackView(frame: .zero, alignment: .center, axis: .vertical, distribution: .fill, spacing: 8.0)
        
        vs.addArrangedSubview(repStack)
        vs.addArrangedSubview(sepLineViewA)
        vs.addArrangedSubview(revStack)
        vs.addArrangedSubview(sepLineViewB)
        vs.addArrangedSubview(ratStack)

        // add vertical stack view to self
        addSubview(vs)
        
        // let's add a little top and bottom "padding"
        //  and fill the width
        vs.snp.makeConstraints { (make) in
            make.top.bottom.equalToSuperview().inset(8.0)
            make.leading.trailing.equalToSuperview()
        }
        
        // set widths for "row" stack views and sep line views
        repStack.snp.makeConstraints { (make) in
            make.width.equalToSuperview().inset(16.0)
        }
        // top separator line extends full width
        sepLineViewA.snp.makeConstraints { (make) in
            make.width.equalToSuperview()
        }
        revStack.snp.makeConstraints { (make) in
            make.width.equalTo(repStack.snp.width)
        }
        sepLineViewB.snp.makeConstraints { (make) in
            make.width.equalTo(repStack.snp.width)
        }
        ratStack.snp.makeConstraints { (make) in
            make.width.equalTo(repStack.snp.width)
        }

        // "row" stack views and sep line views heights
        //  Note: we do NOT set a height for the Reputation stack view
        
        // this will make all three stack views the same heights
        revStack.snp.makeConstraints { (make) in
            make.height.equalTo(repStack.snp.height)
        }
        ratStack.snp.makeConstraints { (make) in
            make.height.equalTo(repStack.snp.height)
        }
        
        // sep line views heights
        sepLineViewA.snp.makeConstraints { (make) in
            make.height.equalTo(1.0)
        }
        sepLineViewB.snp.makeConstraints { (make) in
            make.height.equalTo(1.0)
        }

        if colorBackgrounds {
            repStack.backgroundColor = UIColor(red: 1.0, green: 0.9, blue: 0.9, alpha: 1.0)
            revStack.backgroundColor = UIColor(red: 0.9, green: 1.0, blue: 0.9, alpha: 1.0)
            ratStack.backgroundColor = UIColor(red: 0.9, green: 0.9, blue: 1.0, alpha: 1.0)
            
            reputationLabel.backgroundColor = .yellow
            increasePerformaceLabel.backgroundColor = .cyan
            reviewsLabel.backgroundColor = .green
            dateLabel.backgroundColor = .yellow
            scoreLabel.backgroundColor = .cyan
            ratingsLabel.backgroundColor = .green
            ratingsNumberLabel.backgroundColor = .yellow
        }

    }
    
}

extension UIStackView {
    
    func removeAllSubviews() {
        arrangedSubviews.forEach { $0.removeFromSuperview() }
    }
    
    convenience init(frame: CGRect = .zero,
                     alignment: UIStackView.Alignment = .fill,
                     axis: NSLayoutConstraint.Axis = .vertical,
                     distribution: UIStackView.Distribution = .equalSpacing,
                     spacing: CGFloat = 0.0) {
        
        self.init(frame: frame)
        
        self.alignment = alignment
        self.axis = axis
        self.distribution = distribution
        self.spacing = spacing
    }
}

and an example controller - we'll create Two instances, showing the "debug" colors in the second one:

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        let testView = UserCell(debug: false)
        testView.backgroundColor = .white
        view.addSubview(testView)

        let testViewD = UserCell(debug: true)
        testViewD.backgroundColor = .white
        view.addSubview(testViewD)

        let g = view.safeAreaLayoutGuide
        
        testView.snp.makeConstraints { (make) in
            make.top.leading.trailing.equalTo(g).inset(16.0)
        }
        
        testViewD.snp.makeConstraints { (make) in
            make.top.equalTo(testView.snp.bottom).offset(20.0)
            make.leading.trailing.equalTo(testView)
        }
        
        [testView, testViewD].forEach { v in
            v.layer.cornerRadius = 8.0
            v.layer.shadowColor = UIColor.black.cgColor
            v.layer.shadowRadius = 3.0
            v.layer.shadowOffset = CGSize(width: 0.0, height: 3.0)
            v.layer.shadowOpacity = 0.2
        }
        
    }
    
}

and here's the output:

enter image description here


Edit - in response to comment...

To learn about stack views...

Tip #1:

Then experiment -- a LOT.

Tip #2:

I would also recommend not using SnapKit while learning about UI design and auto-layout. I know many people find it more convenient, but I've seen many beginners using it who don't really understand what it's doing -- and then have a lot of trouble getting the layout to look the way they want.

Tip #3:

As I mentioned previously -- comment your code with what you are expecting to happen. If it doesn't work, it's much easier to figure out why.

Tip #4:

Plan your layout, and then write your code logically to fit that plan. Not to claim that I write the best code in the world, but if you look at how I restructured your UserCell class you'll see that I set up the UI "sections" in order, and then put all the pieces together. That allows you to, for example, code the first section (in this case, the "top row"), run the app, and confirm it's laid out correctly. Then move on to the next section. If you try to code the entire layout all at once, it can be very difficult to realize that a piece of code 50 lines in is conflicting with an earlier line.

Related