How do I fix the Constraints Spacing? - ios

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

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:
Edit - in response to comment...
To learn about stack views...
Tip #1:
start with Apple's docs
head over to google and search for UIStackView tutorials
read through the many, many posts here on SO tagged UIStackView
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

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:
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!
You're doing a few things wrong...
your UI elements should be created and configured in init, not in configure() or layoutSubviews()
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:

Adding UITextView and UIImageView to UIStackView

So, I added some text (UITextView) to my stackView and centered to the top. I also added a UIImageView which would sit nicely under my UITextView. Well it doesn't. For some reason the image covers the text completely. If I delete the image the text comes back up nice on the top center. Played a lot with the stack distribution and alignment but no luck. Not sure what I'm missing :(. Any help is appreciated!
I'm adding both the UITextView and UIIMageView as arrangedSubview to the stack.
Here is my code:
//stack
let stack: UIStackView = {
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.spacing = 5
stack.distribution = .fillProportionally
stack.alignment = .fill
return stack
}()
//text
fileprivate let title: UITextView = {
let title = UITextView()
title.translatesAutoresizingMaskIntoConstraints = false
title.contentMode = .scaleAspectFit
title.layer.cornerRadius = 10
title.backgroundColor = .darkGray
title.font = UIFont(name: "Megrim-Regular", size: 17)
title.textColor = .white
title.textAlignment = .center
return title
}()
//image
let image: UIImageView = {
let image = UIImageView()
image.image = UIImage(named: "demoPic.jpg")
image.translatesAutoresizingMaskIntoConstraints = false
image.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
return image
}()
Hope this below may help,
I think your issue is relating to constraints applied to the stackview and the holder view. (See below)
Your UI Elements (TextView & Image) code seems to be fine (maybe the image will not be work with 50 width /50 height inside this particular stack view configuration. It will require a different approach IMO.
Nevertheless on my playground in order to see it, I just applied 2 constraints towards my container view in order to see your TextView well above your ImageView as you wanted.
Here is the playground I used to reproduce your issue, you can copy and paste it to see if it fits what you request.
import UIKit
import PlaygroundSupport
/// DEMO VIEW CLASS
final class DemoView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented")
}
}
// YOUR UI CODE
//stack
let stack: UIStackView = {
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.spacing = 5
stack.distribution = .fillProportionally
stack.alignment = .fill
return stack
}()
//text
fileprivate let title: UITextView = {
let title = UITextView()
title.translatesAutoresizingMaskIntoConstraints = false
title.contentMode = .scaleAspectFit
title.layer.cornerRadius = 10
title.backgroundColor = .darkGray
title.font = UIFont(name: "Megrim-Regular", size: 17)
title.text = "TextView"
title.textColor = .white
title.textAlignment = .center
return title
}()
//image
let image: UIImageView = {
let image = UIImageView()
image.backgroundColor = .red
image.translatesAutoresizingMaskIntoConstraints = false
image.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
return image
}()
// PLAYGROUND DEMO VIEW TO HOLD YOUR STACK VIEW
let demoView = DemoView(frame: CGRect(x: 0, y: 0, width: 350, height: 150))
stack.addArrangedSubview(title)
stack.addArrangedSubview(image)
demoView.addSubview(stack)
demoView.addConstraints(
NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[stackView]-0-|",
options: NSLayoutConstraint.FormatOptions(rawValue: 0),
metrics: nil,
views: ["stackView": stack])
)
demoView.addConstraints(
NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[stackView]-0-|",
options: NSLayoutConstraint.FormatOptions(rawValue: 0),
metrics: nil,
views: ["stackView": stack])
)
PlaygroundPage.current.liveView = demoView
Results: Your Text View is above the image center (ImageView just have a RED Background here).

Why don't my column separators line up in my table view in my Swift Xcode project?

I have created custom header and custom table view cell classes for my custom table view but the column separators don't line up exactly even though I have the fields the same width in the header and cells. I've noticed that the cell width is 320, while the header width and the table view width are both 311.
Here is my custom table view cell class:
class LOMCell: UITableViewCell {
// MARK: - Properties
lazy var nameLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.textAlignment = .center
label.font = UIFont(name: "AvenirNext-Regular", size: 12.0)
label.backgroundColor = .white
label.layer.borderWidth = 0.25
label.layer.borderColor = UIColor.black.cgColor
label.setWidth(width: 4.0 * self.frame.width / 16.0)
return label
}()
private lazy var containerView: UIView = {
let cv = UIView()
cv.addSubview(ratingImageView)
ratingImageView.anchor(top: cv.topAnchor, left: cv.leftAnchor, bottom: cv.bottomAnchor, right: cv.rightAnchor, paddingTop: 0.0, paddingLeft: 0.0, paddingBottom: 0.0, paddingRight: 0.0)
cv.layer.borderWidth = 0.25
cv.layer.borderColor = UIColor.black.cgColor
cv.setWidth(width: 5.0 * self.frame.width / 16.0 )
return cv
}()
lazy var ratingImageView: UIImageView = {
let iv = UIImageView()
iv.backgroundColor = .white
iv.contentMode = .scaleAspectFit
iv.clipsToBounds = true
return iv
}()
lazy var footprintLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.textAlignment = .center
label.font = UIFont(name: "AvenirNext-Regular", size: 12.0)
label.backgroundColor = .white
label.layer.borderWidth = 0.25
label.layer.borderColor = UIColor.black.cgColor
label.setWidth(width: 3.5 * self.frame.width / 16.0)
return label
}()
lazy var feedbackLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.font = UIFont(name: "AvenirNext-Regular", size: 12.0)
label.textColor = .black
label.backgroundColor = .white
label.layer.borderWidth = 0.25
label.layer.borderColor = UIColor.black.cgColor
label.setWidth(width: 3.5 * self.frame.width / 16.0)
return label
}()
// MARK: - Lifecycle
override init(style: UITableViewCell.CellStyle , reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Helper Functions
private func configureUI() {
let stackView = UIStackView(arrangedSubviews: [nameLabel,
containerView,
footprintLabel,
feedbackLabel])
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.spacing = 0
self.addSubview(stackView)
stackView.anchor(top: self.topAnchor,
left: self.leftAnchor,
bottom: self.bottomAnchor,
right: self.rightAnchor,
paddingTop: 0.0,
paddingLeft: 0.0,
paddingBottom: 0.0,
paddingRight: 0.0)
print("DEBUG: cell width = \(self.frame.width)")
}
}
Here is my custom table view header class:
class LOMHeader: UIView {
// MARK: - Properties
private lazy var nameLabel: UILabel = {
let label = UILabel()
label.text = "Name"
label.textColor = .white
label.textAlignment = .center
label.backgroundColor = .systemGreen
label.font = UIFont(name: "AvenirNext-Bold", size: 12.0)
label.layer.borderWidth = 0.25
label.layer.borderColor = UIColor.black.cgColor
label.setWidth(width: 4.0 * self.frame.width / 16.0)
return label
}()
private lazy var ratingLabel: UILabel = {
let label = UILabel()
label.text = "Rating"
label.textColor = .white
label.textAlignment = .center
label.backgroundColor = .systemGreen
label.font = UIFont(name: "AvenirNext-Bold", size: 12.0)
label.layer.borderWidth = 0.25
label.layer.borderColor = UIColor.black.cgColor
label.setWidth(width: 5.0 * self.frame.width / 16.0)
return label
}()
private lazy var footprintLabel: UILabel = {
let label = UILabel()
label.text = "Footprint"
label.textColor = .white
label.textAlignment = .center
label.backgroundColor = .systemGreen
label.font = UIFont(name: "AvenirNext-Bold", size: 12.0)
label.layer.borderWidth = 0.25
label.layer.borderColor = UIColor.black.cgColor
label.setWidth(width: 3.5 * self.frame.width / 16.0)
return label
}()
private lazy var feedbackLabel: UILabel = {
let label = UILabel()
label.text = "Feedback"
label.textColor = .white
label.textAlignment = .center
label.backgroundColor = .systemGreen
label.font = UIFont(name: "AvenirNext-Bold", size: 12.0)
label.layer.borderWidth = 0.25
label.layer.borderColor = UIColor.black.cgColor
label.setWidth(width: 3.5 * self.frame.width / 16.0)
return label
}()
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
let stackView = UIStackView(arrangedSubviews: [nameLabel,
ratingLabel,
footprintLabel,
feedbackLabel])
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.spacing = 0
self.addSubview(stackView)
stackView.anchor(top: topAnchor,
left: leftAnchor,
bottom: bottomAnchor,
right: rightAnchor,
paddingTop: 0.0,
paddingLeft: 0.0,
paddingBottom: 0.0,
paddingRight: 0.0)
print("DEBUG: header width = \(self.frame.width)")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is my custom view controller with custom table view:
class ListOfMembersVC: UIViewController {
// MARK: - Properties
private let cellID = "ListOfMembersCellID"
private lazy var tableView: UITableView = {
let tv = UITableView()
tv.rowHeight = 40.0
tv.register(LOMCell.self, forCellReuseIdentifier: cellID)
tv.delegate = self
tv.dataSource = self
return tv
}()
private let maxNumberOfRows = 6
private let listOfMembers: [[String : Any]] = [["Name":"Louise", "Rating":UIImage(imageLiteralResourceName: "Rating Stars 2 out of 5"), "Footprint": 2, "Feedback": "??"]]
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
// MARK: - Helper Functions
private func configureUI() {
configureGradientLayer()
title = "List of Members"
navigationController?.navigationBar.barTintColor = .systemBlue
navigationController?.navigationBar.tintColor = .white
navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 30)]
navigationController?.navigationBar.barStyle = .black
view.addSubview(tableView)
tableView.anchor(top: view.safeAreaLayoutGuide.topAnchor,
left: view.leftAnchor,
right: view.rightAnchor,
paddingTop: 40.0,
paddingLeft: 32.0,
paddingRight: 32.0,
height: 60.0 + 40.8 * CGFloat(maxNumberOfRows))
}
}
extension ListOfMembersVC: UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 60.0
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = LOMHeader(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 60.0))
print("DEBUG: table width = \(self.tableView.frame.width)")
return header
}
}
extension ListOfMembersVC: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listOfMembers.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! LOMCell
cell.nameLabel.text = listOfMembers[indexPath.row]["Name"] as? String
cell.ratingImageView.image = (listOfMembers[indexPath.row]["Rating"] as? UIImage)?.withAlignmentRectInsets(UIEdgeInsets(top: -5, left: -10, bottom: -5, right: -10))
cell.footprintLabel.text = String((listOfMembers[indexPath.row]["Footprint"] as? Int)!)
cell.feedbackLabel.text = listOfMembers[indexPath.row]["Feedback"] as? String
print("DEBUG: cell width 2 = \(cell.frame.width)")
return cell
}
}
And here is a screenshot of the problem:
First of all you aren't going to get valid frame sizes until your views are laid out. You'll get more accurate frame sizes in viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("INFO: tableView.width: \(tableView.frame.size.width)")
tableView.visibleCells.compactMap({ $0 as? LOMCell }).forEach {
print("INFO: tableView.cell.width: \($0.frame.size.width)")
}
tableView.subviews.compactMap { $0 as? LOMHeader }.forEach {
print("INFO: tableView.header.width: \($0.frame.size.width)")
}
}
You have the same problem when you try to constrain your subview widths like this. You need to get rid of these.
label.setWidth(width: 4.0 * self.frame.width / 16.0)
The cell's frame property isn't valid until it get's laid out.
What I would do is make the width proportional to its superview to give you more flexibility when the cell or header size changes (such as during rotation):
extension UIView {
func setWidthProportionalToSuperview(by multipler: CGFloat) {
guard let superview = superview else { fatalError("Missing superview") }
widthAnchor.constraint(equalTo: superview.widthAnchor, multiplier: multipler).isActive = true
}
}
You can set up the proportions as soon as you add the subviews to the stackView:
private func configureUI() {
let stackView = UIStackView(arrangedSubviews: [nameLabel,
containerView,
footprintLabel,
feedbackLabel])
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.spacing = 0
self.addSubview(stackView)
stackView.anchor(top: self.topAnchor,
left: self.leftAnchor,
bottom: self.bottomAnchor,
right: self.rightAnchor,
paddingTop: 0.0,
paddingLeft: 0.0,
paddingBottom: 0.0,
paddingRight: 0.0)
nameLabel.setWidthProportionalToSuperview(by: 4.0 / 16.0)
containerView.setWidthProportionalToSuperview(by: 5.0 / 16.0)
footprintLabel.setWidthProportionalToSuperview(by: 3.5 / 16.0)
feedbackLabel.setWidthProportionalToSuperview(by: 3.5 / 16.0)
}
Lastly, you shouldn't add subviews directly to your cell. You should add then as a subview of your contentView:
private func configureUI() {
let stackView = UIStackView(arrangedSubviews: [nameLabel,
containerView,
footprintLabel,
feedbackLabel])
// ...
contentView.addSubview(stackView)
stackView.anchor(top: contentView.topAnchor,
left: contentView.leftAnchor,
bottom: contentView.bottomAnchor,
right: contentView.rightAnchor,
paddingTop: 0.0,
paddingLeft: 0.0,
paddingBottom: 0.0,
paddingRight: 0.0)
}
Your frame values are going to be different when the controller is loaded vs when it's displayed. Call these in your viewDidAppear(_ animated: Bool) to update the frame values or use auto layout on the cell and header subviews.
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
self.tableView.reloadData()

Setting Label below Navigation Bar programmatically in Xcode

recently, I decided to quit using storyboards on my IOS app. So, am presently learning everything now with code. I was trying to place a label below navigationBar view but I got an error and I don't know how to debug it. Please view my code and share your thoughts.
class SellBaseViewController: UIViewController {
lazy var container: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.alignment = .fill
stackView.axis = .vertical
stackView.spacing = 2
stackView.willSetConstraints()
return stackView
}()
lazy var navHeader: UIView! = {
return self.navBar()
}()
lazy var firstLabel: UILabel! = {
return self.labelOne()
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if !Authentication.shared.isAuthenticated {
showLogin()
} else {
self.setupInterface()
}
}
private func setupInterface() {
self.navigationController?.navigationBar.isHidden = true
self.embedInScrollView(content: self.container)
navHeader.willSetConstraints()
firstLabel.willSetConstraints()
self.container.addArrangedSubviews([self.navHeader!, self.firstLabel!])
DispatchQueue.main.async {
NSLayoutConstraint.activate([
self.navHeader.heightAnchor.constraint(equalToConstant: 44),
self.navHeader.widthAnchor.constraint(equalTo: self.view.widthAnchor),
self.navHeader.topAnchor.constraint(equalTo: self.container.topAnchor),
])
}
}
// MARK: NAVBAR
func navBar() -> UIView {
let navBar = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: self.view.frame.width, height: 44)))
navBar.backgroundColor = UIColor.constants.darkBlue
let backIcon = UIImage(named: "ic_back")?.withRenderingMode(.alwaysTemplate)
let returnButton = UIButton(type: .custom)
returnButton.imageView?.tintColor = UIColor.white
returnButton.setImage(backIcon, for: .normal)
returnButton.image(for: .normal)
returnButton.titleLabel?.font = UIFont(name: "Hind", size: 18)
returnButton.setTitle("Sell", for: .normal)
returnButton.setTitleColor(UIColor.white, for: .normal)
returnButton.addTarget(self, action: #selector(self._return), for: .touchUpInside)
returnButton.willSetConstraints()
navBar.addSubviews([returnButton])
NSLayoutConstraint.activate([
returnButton.centerYAnchor.constraint(equalTo: navBar.centerYAnchor),
returnButton.leadingAnchor.constraint(equalTo: navBar.leadingAnchor, constant: 11),
returnButton.heightAnchor.constraint(equalToConstant: 24),
returnButton.widthAnchor.constraint(equalToConstant: 71),
])
return navBar
}
func labelOne() -> UILabel{
let label = UILabel()
label.textAlignment = .center
label.textColor = .white
label.font = UIFont(name: "Avenir-Light", size: 15.0)
label.text = "This is a Label"
self.view.addSubview(label)
return labelOne()
}
#objc func _return() {
self.backHome()
}
}
The navBar showed well, but when I added the label, the app kept crashing with this error. I don't know how to find out exactly what the error is:
Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee6703fe8)
func labelOne() -> UILabel{
let label = UILabel()
label.textAlignment = .center
label.textColor = .white
label.font = UIFont(name: "Avenir-Light", size: 15.0)
label.text = "This is a Label"
self.view.addSubview(label)
return labelOne()
}
You did:
return labelOne()
You should do:
return label

iOS CustomView With AutoLayout in navigationItem not receiving clicks

I created a custom view for navigationItem but somehow it is not receiving any click events:
The code for customView is below
class CustomNavigationView: UIView {
let backButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "icon_back", in: Bundle.main, compatibleWith: nil), for: .normal)
button.isUserInteractionEnabled = true
return button
}()
var profileImage: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "icon_back", in: Bundle.main, compatibleWith: nil)
return imageView
}()
var profileName: UILabel = {
let label = UILabel()
label.text = "No Name"
label.font = UIFont(name: "HelveticaNeue", size: 16) ?? UIFont.systemFont(ofSize: 16)
label.textColor = UIColor(red: 96, green: 94, blue: 94)
return label
}()
var onlineStatusIcon: UIView = {
let view = UIView()
view.backgroundColor = UIColor(28, green: 222, blue: 20)
return view
}()
var onlineStatusText: UILabel = {
let label = UILabel()
label.text = "Online"
label.font = UIFont(name: "HelveticaNeue", size: 12) ?? UIFont.systemFont(ofSize: 12)
label.textColor = UIColor(red: 113, green: 110, blue: 110)
return label
}()
lazy var profileView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [self.profileName, self.onlineStatusText])
stackView.alignment = .fill
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = 2
return stackView
}()
#objc func backButtonClicked(_ sender: UIButton) {
print("Back Button click successfully")
}
private func setupConstraints() {
self.addViewsForAutolayout(views: [backButton, profileImage, onlineStatusIcon, profileView])
//Setup constraints
backButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5).isActive = true
backButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
backButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -10).isActive = true
backButton.widthAnchor.constraint(equalToConstant: 20).isActive = true
profileImage.leadingAnchor.constraint(equalTo: backButton.trailingAnchor, constant: 20).isActive = true
profileImage.topAnchor.constraint(equalTo: self.topAnchor, constant: 5).isActive = true
profileImage.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -5).isActive = true
profileImage.widthAnchor.constraint(equalToConstant: 35).isActive = true
profileImage.layer.cornerRadius = 18
profileImage.clipsToBounds = true
onlineStatusIcon.bottomAnchor.constraint(equalTo: profileImage.bottomAnchor, constant: 0).isActive = true
onlineStatusIcon.leadingAnchor.constraint(equalTo: profileImage.trailingAnchor, constant: -8).isActive = true
onlineStatusIcon.widthAnchor.constraint(equalToConstant: 10).isActive = true
onlineStatusIcon.heightAnchor.constraint(equalToConstant: 10).isActive = true
onlineStatusIcon.layer.cornerRadius = 5
onlineStatusIcon.clipsToBounds = true
profileView.leadingAnchor.constraint(equalTo: profileImage.trailingAnchor, constant: 5).isActive = true
profileView.topAnchor.constraint(equalTo: profileImage.topAnchor).isActive = true
profileView.bottomAnchor.constraint(equalTo: profileImage.bottomAnchor).isActive = true
}
required init() {
super.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 40))
setupConstraints()
addButtonTarget()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addButtonTarget() {
// Setup button callback
backButton.addTarget(self, action: #selector(backButtonClicked(_:)), for: .touchUpInside)
print("Target added")
}
}
And I am setting this view as NavigationbarLeft button Item in my view Controller:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let customView = CustomNavigationView()
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: customView)
}
}
The view is displaying correctly but the clicks are not working at all.
I used view debugging to check if some other layer is on top of this which might be causing problem but nothing of that sort is present.
I also checked backButton frame when adding the target using debug points.
Is there any solution for this problem. Does autolayout not work with custom view in navigation item? Or is there something that I am missing.
You can run the above piece of code and see that the clicks are not working.
This somehow appears to be related to auto layout. If I hardcode the frame position then clicks are working.
class CustomNavigationView: UIView {
let backButton: UIButton = {
let button = UIButton(frame: CGRect(x: 5, y: 5, width: 30, height: 30))
button.setImage(UIImage(named: "icon_back", in: Bundle.kommunicate, compatibleWith: nil), for: .normal)
button.isUserInteractionEnabled = true
return button
}()
var profileImage: UIImageView = {
let imageView = UIImageView(frame: CGRect(x: 40, y: 5, width: 30, height: 30))
imageView.image = UIImage(named: "icon_back", in: Bundle.kommunicate, compatibleWith: nil)
return imageView
}()
var profileName: UILabel = {
let label = UILabel(frame: CGRect(x: 80, y: 5, width: 50, height: 15))
label.text = "No Name"
label.font = UIFont(name: "HelveticaNeue", size: 16) ?? UIFont.systemFont(ofSize: 16)
label.textColor = UIColor(red: 96, green: 94, blue: 94)
return label
}()
var onlineStatusIcon: UIView = {
let view = UIView(frame: CGRect(x: 65, y: 30, width: 10, height: 10))
view.backgroundColor = UIColor(28, green: 222, blue: 20)
return view
}()
var onlineStatusText: UILabel = {
let label = UILabel(frame: CGRect(x: 80, y: 25, width: 50, height: 10))
label.text = "Online"
label.font = UIFont(name: "HelveticaNeue", size: 12) ?? UIFont.systemFont(ofSize: 12)
label.textColor = UIColor(red: 113, green: 110, blue: 110)
return label
}()
lazy var profileView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [self.profileName, self.onlineStatusText])
stackView.alignment = .fill
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = 2
return stackView
}()
#objc func backButtonClicked(_ sender: UIButton) {
print("Back button is successfully called")
}
private func setupConstraints() {
self.addSubview(backButton)
self.addSubview(profileImage)
self.addSubview(onlineStatusIcon)
self.addSubview(profileView)
}
required init() {
super.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 40))
setupConstraints()
addButtonTarget()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addButtonTarget() {
// Setup button callback
backButton.addTarget(self, action: #selector(backButtonClicked(_:)), for: .touchUpInside)
print("Target added")
}
}
The problem is with the manually added constraints that you added.
Using the view debugger the width of CustomNavigationView after it is added to the bar is 0.
In order to force the container to expand, add the following constraint in setupConstraints():
profileView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
Now that the container expands to match it's contents, the touch events should be propagated to the button as expected.

Resources