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
CodePudding user response:
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: