iOS - Take all the available space in a horizontal UIStackView - ios

I have UITableViewCell that contains a horizontal UIStackView. The UIStackView contains four views in the following order.
UIImageView UILabel UILabel UIImageView
There are 16 points spacing after the arrangedSubViews. I want that the second UILabel takes all the available space. If there is not enough space, it text should wrap.
I have used the following codes. It works almost. The problem is that even though there is enough space between the UILabel's as you see in the screenshot attached, the second UILabel breaks. But I want it to break only if there is not enough space.
class TableViewCell: UITableViewCell {
static let identifier = "TableViewCell"
private let leadingImageView: UIImageView = {
let view = UIImageView(image: UIImage(systemName: "calendar"))
view.setConstraints(heightConstant: 25, widthConstant: 25)
view.tintColor = .text
return view
}()
private let leadingLabel: UILabel = {
let label = UILabel()
label.textColor = .label
label.text = "Start Date"
label.numberOfLines = 0
label.sizeToFit()
return label
}()
let trailingLabel: UILabel = {
let label = UILabel()
label.text = "Friday, 17 July 2020"
label.textColor = .label
label.numberOfLines = 0
label.textAlignment = .right
return label
}()
let trailingImageView: UIImageView = {
let configuration = UIImage.SymbolConfiguration(pointSize: 12, weight: .light)
let image = UIImage(systemName: "arrowtriangle.down", withConfiguration: configuration)
let view = UIImageView(image: image)
return view
}()
private let superStackView: UIStackView = {
let view = UIStackView()
view.distribution = .fillProportionally
view.alignment = .center
view.spacing = 16
return view
}()
private let containerView: UIView = {
let view = UIView()
view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
view.setContentHuggingPriority(.defaultHigh, for: .vertical)
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
extension TableViewCell {
private func setUpSubviews(){
containerView.addSubview(trailingImageView)
trailingImageView.alignCenter(centerXAnchor: containerView.centerXAnchor,
centerYAnchor: containerView.centerYAnchor)
superStackView.addArrangedSubview(leadingImageView)
superStackView.addArrangedSubview(leadingLabel)
superStackView.addArrangedSubview(trailingLabel)
superStackView.addArrangedSubview(containerView)
let constant = CGFloat(16)
self.addSubview(superStackView)
self.contentView.addSubview(superStackView)
superStackView.setConstraints(topAnchor: contentView.topAnchor, leadingAnchor: contentView.leadingAnchor,
bottomAnchor: contentView.bottomAnchor, trailingAnchor: contentView.trailingAnchor,
topConstant: constant, leadingConstant: constant, bottomConstant: constant, trailingConstant: constant)
self.contentView.updateConstraints()
}
}
How can I fix this issue so that the cell looks like the cells in the following image?

You're close...
First, setting a stack view's Distribution to Fill Proportionally is the most misunderstood distribution option, and you don't want to use it here.
Second, it helps greatly when designing to use contrasting backgrounds to make it easy to see what your frames are doing.
Here is your code, modified to get to what appears to be your goal. I don't have your "constraint helpers" so I changed it to standard constraint format. I also added a few comments for some clarification:
class TableViewCell: UITableViewCell {
static let identifier = "TableViewCell"
private let leadingImageView: UIImageView = {
let view = UIImageView(image: UIImage(systemName: "calendar"))
view.translatesAutoresizingMaskIntoConstraints = false
//view.setConstraints(heightConstant: 25, widthConstant: 25)
view.widthAnchor.constraint(equalToConstant: 25).isActive = true
view.heightAnchor.constraint(equalToConstant: 25).isActive = true
view.tintColor = .blue // .text
return view
}()
private let leadingLabel: UILabel = {
let label = UILabel()
label.textColor = .label
label.text = "Start Date"
// only single line for "leading label"
label.numberOfLines = 1
// content hugging
label.setContentHuggingPriority(.required, for: .horizontal)
label.backgroundColor = .cyan
return label
}()
let trailingLabel: UILabel = {
let label = UILabel()
label.text = "Friday, 17 July 2020"
label.textColor = .label
label.numberOfLines = 0
label.textAlignment = .right
label.backgroundColor = .green
return label
}()
let trailingImageView: UIImageView = {
let configuration = UIImage.SymbolConfiguration(pointSize: 12, weight: .light)
let image = UIImage(systemName: "arrowtriangle.down", withConfiguration: configuration)
let view = UIImageView(image: image)
return view
}()
private let superStackView: UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
// do NOT use .fillProportionally
view.distribution = .fill
view.alignment = .center
view.spacing = 16
return view
}()
private let containerView: UIView = {
let view = UIView()
// not needed
//view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
//view.setContentHuggingPriority(.defaultHigh, for: .vertical)
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
extension TableViewCell {
private func setUpSubviews(){
containerView.addSubview(trailingImageView)
//trailingImageView.alignCenter(centerXAnchor: containerView.centerXAnchor,
// centerYAnchor: containerView.centerYAnchor)
superStackView.addArrangedSubview(leadingImageView)
superStackView.addArrangedSubview(leadingLabel)
superStackView.addArrangedSubview(trailingLabel)
superStackView.addArrangedSubview(containerView)
let constant = CGFloat(16)
self.addSubview(superStackView)
self.contentView.addSubview(superStackView)
//superStackView.setConstraints(topAnchor: contentView.topAnchor, leadingAnchor: contentView.leadingAnchor,
// bottomAnchor: contentView.bottomAnchor, trailingAnchor: contentView.trailingAnchor,
// topConstant: constant, leadingConstant: constant, bottomConstant: constant, trailingConstant: constant)
NSLayoutConstraint.activate([
trailingImageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
trailingImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
superStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: constant),
superStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: constant),
superStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -constant),
superStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -constant),
])
// not needed
//self.contentView.updateConstraints()
}
}
And, an example view controller:
class MahanTableViewController: UITableViewController {
var myData: [String] = [
"Saturday, 18 July 2020",
"Sunday, 19 July 2020",
"Wednesday, 22 July 2020",
"Saturday, 26 September 2020",
"Sunday, 27 September 2020",
"Wednesday, 30 September 2020",
]
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.identifier)
}
// MARK: - Table view data source
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: TableViewCell.identifier, for: indexPath) as! TableViewCell
cell.trailingLabel.text = myData[indexPath.row]
return cell
}
}
Producing this output:

Try to Return 1
let trailingLabel: UILabel = {
let label = UILabel()
label.text = "Friday, 17 July 2020"
label.textColor = .label
label.numberOfLines = 1
label.textAlignment = .right
return label
}()

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:

Resizing image inside of UIButton

I have been trying to get the system images I placed inside of a UIButton to scale aspect fit, but no solutions I find online seem to work. This is what I have tried so far:
//the cells objects
var acceptButton = UIButton(type: .custom)
var declineButton = UIButton(type: .custom)
override init(frame: CGRect) {
super.init(frame: frame)
//add the contents and set its autolayout constraints
setAutoLayoutConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//function to set the autolayout constraints for the cell's contents
func setAutoLayoutConstraints() {
//create the stack view which will contain the accept and reject buttons
let buttonStackView = UIStackView()
buttonStackView.axis = .horizontal
buttonStackView.distribution = .fillEqually
buttonStackView.alignment = .fill
//add the buttons to the stack view
buttonStackView.addArrangedSubview(acceptButton)
buttonStackView.addArrangedSubview(declineButton)
//add the button stack view to the cell
self.addSubview(buttonStackView)
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
buttonStackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
buttonStackView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
buttonStackView.leadingAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
buttonStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
//add the button images
let addImage = UIImage(systemName: "checkmark.circle")
acceptButton.imageView?.contentMode = .scaleAspectFit
acceptButton.setImage(addImage, for: .normal)
acceptButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
acceptButton.addTarget(self, action: #selector(addButtonClicked), for: .touchUpInside)
let declineImage = UIImage(systemName: "xmark.octagon")
declineButton.imageView?.contentMode = .scaleAspectFit
declineButton.setImage(declineImage, for: .normal)
declineButton.addTarget(self, action: #selector(declineButtonClicked), for: .touchUpInside)
}
This is what I get:
(The "test test" label is just what I added for the left half of the cell's contents, can be ignored)
I have tried multiple solutions such as setting the edge insets, setting the content mode of the button and the button's image view, using a custom button, etc. but I can't seem to get the image to resized.
NOTE: When I make the image the background image with setBackgroundImage, I am able to resize the button but I can not scale aspect fit it, only fill it horizontally and vertically which is not what I am looking to do.
Any help is appreciated, thanks!!
This is a simple solution example: add first your labelTest and after that set stackView leading anchor to labelTest trailing...
this is my tableView:
class YourViewController: UIViewController, UITabBarDelegate, UITableViewDataSource, UITableViewDelegate {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
tableView.delegate = self
tableView.dataSource = self
tableView.register(myCell.self, forCellReuseIdentifier: "cellId")
tableView.backgroundColor = .white
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! myCell
return cell
}
}
Now set my cell with arrangedSubviews and set constraints to content view (this is the correct way, not directly to cell):
class myCell: UITableViewCell {
let labelTest: UILabel = {
let label = UILabel()
label.text = "test Label"
label.backgroundColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var acceptButton = UIButton(type: .custom)
var declineButton = UIButton(type: .custom)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
acceptButton.backgroundColor = .purple
declineButton.backgroundColor = .brown
setAutoLayoutConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//function to set the autolayout constraints for the cell's contents
func setAutoLayoutConstraints() {
contentView.addSubview(labelTest)
labelTest.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
labelTest.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
labelTest.trailingAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
labelTest.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
let buttonStackView = UIStackView(arrangedSubviews: [acceptButton, declineButton])
buttonStackView.axis = .horizontal
buttonStackView.distribution = .fillEqually
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
//add the button stack view to the cell
contentView.addSubview(buttonStackView)
buttonStackView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
buttonStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
buttonStackView.leadingAnchor.constraint(equalTo: labelTest.trailingAnchor).isActive = true
buttonStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
//add the buttons images
let addImage = UIImage(systemName: "checkmark.circle")
acceptButton.imageView?.contentMode = .scaleAspectFit
acceptButton.setImage(addImage, for: .normal)
acceptButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
acceptButton.addTarget(self, action: #selector(addButtonClicked), for: .touchUpInside)
let declineImage = UIImage(systemName: "xmark.octagon")
declineButton.imageView?.contentMode = .scaleAspectFit
declineButton.setImage(declineImage, for: .normal)
declineButton.addTarget(self, action: #selector(declineButtonClicked), for: .touchUpInside)
}
}
If you want some space to left/right simply add constant to relative constraint:
labelTest.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10).isActive = true
buttonStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10).isActive = true
This is the results

Multilinelabel inside multiple stackviews inside UITableViewCell

I have view hierarchy like below;
UITableViewCell ->
-> UIView -> UIStackView (axis: vertical, distribution: fill)
-> UIStackView (axis: horizontal, alignment: top, distribution: fillEqually)
-> UIView -> UIStackView(axis:vertical, distribution: fill)
-> TwoLabelView
My problem is that labels don't get more than one line. I read every question in SO and also tried every possibility but none of them worked. On below screenshot, on the top left box, there should be two pair of label but even one of them isn't showing.
My Question is that how can I achieve multiline in the first box (both for left and right)?
If I change top stack views distribution to fillProportionally, labels get multiline but there will be a gap between last element of first box and the box itself
My first top stack views
//This is the Stackview used just below UITableViewCell
private let stackView: UIStackView = {
let s = UIStackView()
s.distribution = .fill
s.axis = .vertical
s.spacing = 10
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
//This is used to create two horizontal box next to each other
private let myStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fillEqually
s.spacing = 10
s.axis = .horizontal
//s.alignment = .center
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
UILabel Class:
fileprivate class FixAutoLabel: UILabel {
override func layoutSubviews() {
super.layoutSubviews()
if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width
}
}
}
#IBDesignable class TwoLabelView: UIView {
var topMargin: CGFloat = 0.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 0.0
#IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
#IBInspectable var secondLabelText: String = "" { didSet { updateView() } }
fileprivate var firstLabel: FixAutoLabel!
fileprivate var secondLabel: FixAutoLabel!
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required public init?(coder: NSCoder) {
super.init(coder:coder)
setUpView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
}
func setUpView() {
firstLabel = FixAutoLabel()
firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.bold)
firstLabel.numberOfLines = 0
firstLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
secondLabel = FixAutoLabel()
secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFont.Weight.regular)
secondLabel.numberOfLines = 1
secondLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
addSubview(firstLabel)
addSubview(secondLabel)
// we're going to set the constraints
firstLabel .translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
// pin both labels' left-edges to left-edge of self
firstLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
secondLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
// pin both labels' right-edges to right-edge of self
firstLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
secondLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
// pin firstLabel to the top of self + topMargin (padding)
firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin).isActive = true
// pin top of secondLabel to bottom of firstLabel + verticalSpacing
secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing).isActive = true
// pin bottom of self to bottom of secondLabel + bottomMargin (padding)
bottomAnchor.constraint(equalTo: secondLabel.bottomAnchor, constant: bottomMargin).isActive = true
// call common "refresh" func
updateView()
}
func updateView() {
firstLabel.preferredMaxLayoutWidth = self.bounds.width
secondLabel.preferredMaxLayoutWidth = self.bounds.width
firstLabel.text = firstLabelText
secondLabel.text = secondLabelText
firstLabel.sizeToFit()
secondLabel.sizeToFit()
setNeedsUpdateConstraints()
}
override open var intrinsicContentSize : CGSize {
// just has to have SOME intrinsic content size defined
// this will be overridden by the constraints
return CGSize(width: 1, height: 1)
}
}
UIView -> UIStackView class
class ViewWithStack: UIView {
let verticalStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fillEqually
s.spacing = 10
s.axis = .vertical
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.white
self.layer.cornerRadius = 6.0
self.layer.applySketchShadow(color: UIColor(red:0.56, green:0.56, blue:0.56, alpha:1), alpha: 0.2, x: 0, y: 0, blur: 10, spread: 0)
addSubview(verticalStackView)
let lessThan = verticalStackView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: 0)
lessThan.priority = UILayoutPriority(1000)
lessThan.isActive = true
verticalStackView.leftAnchor.constraint(equalTo: self.leftAnchor,constant: 0).isActive = true
verticalStackView.rightAnchor.constraint(equalTo: self.rightAnchor,constant: 0).isActive = true
verticalStackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
verticalStackView.layoutMargins = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
verticalStackView.isLayoutMarginsRelativeArrangement = true
}
convenience init(orientation: NSLayoutConstraint.Axis,labelsArray: [UIView]) {
self.init()
verticalStackView.axis = orientation
for label in labelsArray {
verticalStackView.addArrangedSubview(label)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Example Controller Class (This is a minimized version of the whole project):
class ViewController: UIViewController, UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let viewWithStack = BoxView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.delegate = self
tableView.dataSource = self
tableView.register(TableViewCell.self, forCellReuseIdentifier: "myCell")
tableView.rowHeight = UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TableViewCell = tableView.dequeueReusableCell(withIdentifier: "myCell") as! TableViewCell
if (indexPath.row == 0) {
cell.setup(viewWithStack: self.viewWithStack)
} else {
cell.backgroundColor = UIColor.black
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
//return 500
if ( indexPath.row == 0) {
return UITableView.automaticDimension
} else {
return 40
}
}
}
EDIT I created a minimal project then I found that my problem is that my project implements heightForRow function which overrides UITableViewAutomaticDimension so that It gives wrong height for my component. I think I should look how to get height size of the component? because I can't delete heightForRow function which solves my problem.
Example Project Link https://github.com/emreond/tableviewWithStackView/tree/master/tableViewWithStackViewEx
Example project has ambitious layouts when you open view debugger. I think when I fix them, everything should be fine.
Here is a full example that should do what you want (this is what I mean by a minimal reproducible example):
Best way to examine this is to:
create a new project
create a new file, named TestTableViewController.swift
copy and paste the code below into that file (replace the default template code)
add a UITableViewController to the Storyboard
assign its Custom Class to TestTableViewController
embed it in a UINavigationController
set the UINavigationController as Is Initial View Controller
run the app
This is what you should see as the result:
I based the classes on what you had posted (removed unnecessary code, and I am assuming you have the other cells working as desired).
//
// TestTableViewController.swift
//
// Created by Don Mag on 10/21/19.
//
import UIKit
class SideBySideCell: UITableViewCell {
let horizStackView: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fillEqually
v.spacing = 10
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func prepareForReuse() {
horizStackView.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
}
func commonInit() -> Void {
contentView.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
contentView.addSubview(horizStackView)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
horizStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
horizStackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
horizStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
horizStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
])
}
func addViewWithStack(_ v: ViewWithStack) -> Void {
horizStackView.addArrangedSubview(v)
}
}
class TestTableViewController: UITableViewController {
let sideBySideReuseID = "sbsID"
override func viewDidLoad() {
super.viewDidLoad()
// register custom SideBySide cell for reuse
tableView.register(SideBySideCell.self, forCellReuseIdentifier: sideBySideReuseID)
tableView.separatorStyle = .none
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: sideBySideReuseID, for: indexPath) as! SideBySideCell
let twoLabelView1 = TwoLabelView()
twoLabelView1.firstLabelText = "Text for first label on left-side."
twoLabelView1.secondLabelText = "10.765,00TL"
let twoLabelView2 = TwoLabelView()
twoLabelView2.firstLabelText = "Text for second-first label on left-side."
twoLabelView2.secondLabelText = "10.765,00TL"
let twoLabelView3 = TwoLabelView()
twoLabelView3.firstLabelText = "Text for the first label on right-side."
twoLabelView3.secondLabelText = "10.765,00TL"
let leftStackV = ViewWithStack(orientation: .vertical, labelsArray: [twoLabelView1, twoLabelView2])
let rightStackV = ViewWithStack(orientation: .vertical, labelsArray: [twoLabelView3])
cell.addViewWithStack(leftStackV)
cell.addViewWithStack(rightStackV)
return cell
}
// create ViewWithStack using just a simple label
let cell = tableView.dequeueReusableCell(withIdentifier: sideBySideReuseID, for: indexPath) as! SideBySideCell
let v = UILabel()
v.text = "This is row \(indexPath.row)"
let aStackV = ViewWithStack(orientation: .vertical, labelsArray: [v])
cell.addViewWithStack(aStackV)
return cell
}
}
#IBDesignable class TwoLabelView: UIView {
var topMargin: CGFloat = 0.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 0.0
#IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
#IBInspectable var secondLabelText: String = "" { didSet { updateView() } }
fileprivate var firstLabel: UILabel = {
let v = UILabel()
return v
}()
fileprivate var secondLabel: UILabel = {
let v = UILabel()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required public init?(coder: NSCoder) {
super.init(coder:coder)
setUpView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
}
func setUpView() {
firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.bold)
firstLabel.numberOfLines = 0
secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFont.Weight.regular)
secondLabel.numberOfLines = 1
addSubview(firstLabel)
addSubview(secondLabel)
// we're going to set the constraints
firstLabel .translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
// Note: recommended to use Leading / Trailing rather than Left / Right
NSLayoutConstraint.activate([
// pin both labels' left-edges to left-edge of self
firstLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
secondLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
// pin both labels' right-edges to right-edge of self
firstLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
secondLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// pin firstLabel to the top of self + topMargin (padding)
firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin),
// pin top of secondLabel to bottom of firstLabel + verticalSpacing
secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing),
// pin bottom of self to >= (bottom of secondLabel + bottomMargin (padding))
bottomAnchor.constraint(greaterThanOrEqualTo: secondLabel.bottomAnchor, constant: bottomMargin),
])
}
func updateView() -> Void {
firstLabel.text = firstLabelText
secondLabel.text = secondLabelText
}
}
class ViewWithStack: UIView {
let verticalStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fill
s.spacing = 10
s.axis = .vertical
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.white
self.layer.cornerRadius = 6.0
// self.layer.applySketchShadow(color: UIColor(red:0.56, green:0.56, blue:0.56, alpha:1), alpha: 0.2, x: 0, y: 0, blur: 10, spread: 0)
addSubview(verticalStackView)
NSLayoutConstraint.activate([
// constrain to all 4 sides
verticalStackView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
verticalStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
verticalStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
verticalStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
])
verticalStackView.layoutMargins = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
verticalStackView.isLayoutMarginsRelativeArrangement = true
}
convenience init(orientation: NSLayoutConstraint.Axis, labelsArray: [UIView]) {
self.init()
verticalStackView.axis = orientation
for label in labelsArray {
verticalStackView.addArrangedSubview(label)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Prevent UIStackView from compressing UITableView

I am adding a UITableView into vertical UIStackView. That vertical UIStackView is within a UIScrollView.
However the table is not displaying unless I force an explicit Height constraint on it which I obviously don't want to do.
According to this SO question and answer UITableView not shown inside UIStackView this is because "stackview tries to compress content as much as possible"
If I add a UILabel to the StackView it is displayed fine. There is something specific about the UITableView that means it is not. I am using Xamarin and creating the UITableView in code
this.recentlyOpenedPatientsTable = new UITableView()
{
RowHeight = UITableView.AutomaticDimension,
EstimatedRowHeight = 44.0f,
AllowsMultipleSelectionDuringEditing = false,
TranslatesAutoresizingMaskIntoConstraints = false,
Editing = false,
BackgroundColor = UIColor.Clear,
TableFooterView = new UIView(),
ScrollEnabled = false,
};
The UIScrollView is pinned to the Top, Bottom, Left and Right of the View and works fine. It takes the Height I expect.
I have tried both the suggestions in this SO question and neither have worked. I find it odd that I cannot find others having this issue.
Any other suggestions?
Here is a very basic example, using a UITableView subclass to make it auto-size its height based on its content.
The red buttons (in a horizontal stack view) are the first arranged subView in the vertical stack view.
The table is next (green background for the cells' contentView, yellow background for a multi-line label).
And the last arranged subView is a cyan background UILabel:
Note that the vertical stack view is constrained 40-pts from Top, Leading and Trailing, and at least 40-pts from the Bottom. If you add enough rows to the table to exceed the available height, you'll have to scroll to see the additional rows.
//
// TableInStackViewController.swift
//
// Created by Don Mag on 6/24/19.
//
import UIKit
final class ContentSizedTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
class TableInStackCell: UITableViewCell {
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .yellow
v.textAlignment = .left
v.numberOfLines = 0
return v
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = .green
contentView.addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor, constant: 0.0),
theLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: 0.0),
theLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor, constant: 0.0),
theLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor, constant: 0.0),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TableInStackViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 8
return v
}()
let addButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Add a Row", for: .normal)
v.backgroundColor = .red
return v
}()
let deleteButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Delete a Row", for: .normal)
v.backgroundColor = .red
return v
}()
let buttonsStack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fillEqually
v.spacing = 20
return v
}()
let theTable: ContentSizedTableView = {
let v = ContentSizedTableView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let bottomLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
v.textAlignment = .center
v.numberOfLines = 0
v.text = "This label is the last element in the stack view."
// prevent label from being compressed when the table gets too tall
v.setContentCompressionResistancePriority(.required, for: .vertical)
return v
}()
var theTableData: [String] = [
"Content Sized Table View",
"This row shows that the cell heights will auto-size, based on the cell content (multi-line label in this case).",
"Here is the 3rd default row",
]
var minRows = 1
let reuseID = "TableInStackCell"
override func viewDidLoad() {
super.viewDidLoad()
minRows = theTableData.count
view.addSubview(theStackView)
NSLayoutConstraint.activate([
// constrain stack view 40-pts from top, leading and trailing
theStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40.0),
theStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40.0),
// constrain stack view *at least* 40-pts from bottom
theStackView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40.0),
])
buttonsStack.addArrangedSubview(addButton)
buttonsStack.addArrangedSubview(deleteButton)
theStackView.addArrangedSubview(buttonsStack)
theStackView.addArrangedSubview(theTable)
theStackView.addArrangedSubview(bottomLabel)
theTable.delegate = self
theTable.dataSource = self
theTable.register(TableInStackCell.self, forCellReuseIdentifier: reuseID)
addButton.addTarget(self, action: #selector(addRow), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(deleteRow), for: .touchUpInside)
}
#objc func addRow() -> Void {
// add a row to our data source
let n = theTableData.count - minRows
theTableData.append("Added Row: \(n + 1)")
theTable.reloadData()
}
#objc func deleteRow() -> Void {
// delete a row from our data source (keeping the original rows intact)
let n = theTableData.count
if n > minRows {
theTableData.remove(at: n - 1)
theTable.reloadData()
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theTableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseID, for: indexPath) as! TableInStackCell
cell.theLabel.text = theTableData[indexPath.row]
return cell
}
}

UITableViewCell constraints for dynamic height break

I have a simple UITableViewCell subclass in which I have a titleLabel property (the cell has more views, but for the sake of showing the issue, I will only do one label as it also breaks).
Here is my label code:
self.titleLabel = UILabel(frame: .zero)
self.titleLabel.numberOfLines = 0
self.titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
self.titleLabel.textColor = UIColor.white
self.titleLabel.adjustsFontSizeToFitWidth = false
self.titleLabel.textAlignment = .left
self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.titleLabel)
self.titleLabel.topAnchor.constraint(equalTo: self.artworkImageView.topAnchor).isActive = true
self.titleLabel.leftAnchor.constraint(equalTo: self.artworkImageView.rightAnchor, constant: 10.0).isActive = true
self.titleLabel.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -10.0).isActive = true
self.titleLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor).isActive = true
I also set my UITableView up like this:
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.estimatedRowHeight = 50.0
However it keeps breaking constraints with an error like this:
"<NSLayoutConstraint:0x28211ce10 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x10859a4f0.height == 4.33333 (active)>"
There are more constraints, however this one says that my cell content view is only 4.3 of height, however I want it to grow as the label grows.
I also tried setting contentHuggingPriorities and the priority of the bottom anchor. I also compared it to code online or IB constraints I saw online and they all set 4 constraints: top, left, bottom, right.
I also tried leading and trailing instead of left and right - same result.
Any help appreciated
Here is my full AlbumTableViewCell:
class AlbumTableViewCell: UITableViewCell {
public private(set) var artworkImageView: UIImageView
public private(set) var titleLabel: UILabel
public private(set) var albumInfoLabel: UILabel
public private(set) var artistNameLabel: UILabel
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
self.artworkImageView = UIImageView(frame: .zero)
self.titleLabel = UILabel(frame: .zero)
self.albumInfoLabel = UILabel(frame: .zero)
self.artistNameLabel = UILabel(frame: .zero)
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.tintColor = UIColor.white
self.backgroundColor = UIColor.clear
self.contentView.backgroundColor = UIColor.barTintColor
self.contentView.layer.masksToBounds = false
self.contentView.layer.cornerRadius = 10.0
self.artworkImageView.layer.cornerRadius = 10.0
self.artworkImageView.layer.masksToBounds = true
self.artworkImageView.contentMode = .scaleAspectFit
self.artworkImageView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.artworkImageView)
// image view
self.artworkImageView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 5).isActive = true
self.artworkImageView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 5).isActive = true
self.artworkImageView.widthAnchor.constraint(equalToConstant: 80).isActive = true
self.artworkImageView.heightAnchor.constraint(equalToConstant: 80).isActive = true
self.titleLabel = UILabel(frame: .zero)
self.titleLabel.numberOfLines = 2
self.titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
self.titleLabel.textColor = UIColor.white
self.titleLabel.adjustsFontSizeToFitWidth = false
self.titleLabel.textAlignment = .left
self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.titleLabel)
// title
self.titleLabel.leadingAnchor.constraint(equalTo: self.artworkImageView.trailingAnchor, constant: 5.0).isActive = true
self.titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 5.0).isActive = true
self.titleLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -5.0).isActive = true
self.titleLabel.heightAnchor.constraint(equalToConstant: 35).isActive = true
self.albumInfoLabel.numberOfLines = 1
self.albumInfoLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
self.albumInfoLabel.textColor = UIColor.lightGray
self.albumInfoLabel.adjustsFontSizeToFitWidth = true
self.albumInfoLabel.textAlignment = .left
self.albumInfoLabel.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.albumInfoLabel)
// albumInfoLabel
self.albumInfoLabel.topAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: 5.0).isActive = true
self.albumInfoLabel.leadingAnchor.constraint(equalTo: self.titleLabel.leadingAnchor).isActive = true
self.albumInfoLabel.trailingAnchor.constraint(equalTo: self.titleLabel.trailingAnchor).isActive = true
self.albumInfoLabel.heightAnchor.constraint(equalToConstant: 35).isActive = true
self.artistNameLabel = UILabel(frame: .zero)
self.artistNameLabel.numberOfLines = 1
self.artistNameLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
self.artistNameLabel.textColor = UIColor.lightGray
self.artistNameLabel.adjustsFontSizeToFitWidth = true
self.artistNameLabel.textAlignment = .left
self.artistNameLabel.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.artistNameLabel)
// albumInfoLabel
self.artistNameLabel.topAnchor.constraint(equalTo: self.albumInfoLabel.bottomAnchor, constant: 5.0).isActive = true
self.artistNameLabel.leadingAnchor.constraint(equalTo: self.albumInfoLabel.leadingAnchor).isActive = true
self.artistNameLabel.trailingAnchor.constraint(equalTo: self.albumInfoLabel.trailingAnchor).isActive = true
self.artistNameLabel.heightAnchor.constraint(equalToConstant: 35).isActive = true
let selectedView: UIView = UIView(frame: .zero)
selectedView.backgroundColor = UIColor.gray
selectedView.layer.cornerRadius = 10.0
selectedView.layer.masksToBounds = false
self.selectedBackgroundView = selectedView
}
override func layoutSubviews() {
super.layoutSubviews()
let contentViewFrame = self.contentView.frame
let insetContentViewFrame = contentViewFrame.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))
self.contentView.frame = insetContentViewFrame
self.selectedBackgroundView?.frame = insetContentViewFrame
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
This code does not crash anymore but the cell does not autoresize (see image).. The light gray area is the content view
this code does not break any constrains anymore, but the cell also does not calculate the hight automatically. Here is my table view controller:
self.tableView.register(AlbumTableViewCell.self, forCellReuseIdentifier: "AlbumCell")
self.tableView.separatorStyle = .none
self.tableView.tableFooterView = UIView(frame: .zero)
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.estimatedRowHeight = 50.0
var titleLabel = UILabel()
contentView.addSubview(titleLabel)
titleLabel.textColor = UIColor(red:0.32, green:0.17, blue:0.12, alpha:1.0)
titleLabel.font = UIFont.boldSystemFont(ofSize: 16.0)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.topAnchor.constraint(equalTo: marginGuide.topAnchor).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: marginGuide.trailingAnchor, constant: 8).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor).isActive = true
titleLabel.leadingAnchor.constraint(equalTo: marginGuide.leadingAnchor, constant: 8).isActive = true
Try this.
To be honest i don't know exactly what's your issue but i know for sure that you have a bad time with cell constraint and you want a dynamic cell .
So let's say you have a cell with 4 views artWrokImageView , artNameLabel , artDescriptionLabel and the artistNameLabel
First you need to make sure these views most constraint from top and bottom the table cell , So when you call self.tableView.rowHeight = UITableView.automaticDimension it knows how to dynamically expand .
Second you need to tell the table to expand when ever view did appear
This is demo for the 4 views above .
Table View Controller :
class YourTableViewController : UITableViewController {
let customTableCellID = "customTableCellID";
override func viewDidLoad() {
super.viewDidLoad();
setupTable();
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tableView.estimatedRowHeight = 50;
self.tableView.rowHeight = UITableView.automaticDimension;
}
fileprivate func setupTable() {
tableView.register(YourCustomTableCell.self, forCellReuseIdentifier: customTableCellID);
}
}
extension YourTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1;
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1;
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: customTableCellID, for: indexPath) as! YourCustomTableCell
cell.artistNameLabel.text = "the real art";
cell.artworkImageView.image = UIImage(named: "mazen");
cell.artDescriptionLabel.text = "long long long long long long long long long long long long long long long long long long long long long long long long long description";
cell.artNameLabel.text = "someting"
return cell
}
}
Cell :
class YourCustomTableCell : UITableViewCell {
var artworkImageView : UIImageView = {
let imageView = UIImageView();
imageView.translatesAutoresizingMaskIntoConstraints = false;
return imageView;
}()
var artNameLabel : UILabel = {
let label = UILabel();
label.font = UIFont.boldSystemFont(ofSize: 20);
label.translatesAutoresizingMaskIntoConstraints = false;
return label
}()
var artDescriptionLabel : UILabel = {
let label = UILabel();
label.textColor = .darkGray;
label.numberOfLines = 0;
label.translatesAutoresizingMaskIntoConstraints = false;
return label;
}()
var artistNameLabel : UILabel = {
let label = UILabel();
label.textColor = .blue;
label.translatesAutoresizingMaskIntoConstraints = false;
return label;
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier);
setupCell();
}
fileprivate func setupCell() {
// add views
contentView.addSubview(artworkImageView);
contentView.addSubview(artNameLabel);
contentView.addSubview(artDescriptionLabel);
contentView.addSubview(artistNameLabel);
// layout views
// image view
artworkImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5).isActive = true;
artworkImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true;
artworkImageView.widthAnchor.constraint(equalToConstant: 80).isActive = true;
artworkImageView.heightAnchor.constraint(equalToConstant: 80).isActive = true;
// art name
artNameLabel.leadingAnchor.constraint(equalTo: artworkImageView.trailingAnchor, constant: 5).isActive = true;
artNameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true;
artNameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5).isActive = true;
artNameLabel.heightAnchor.constraint(equalToConstant: 35).isActive = true;
// descripion
artDescriptionLabel.leadingAnchor.constraint(equalTo: artworkImageView.trailingAnchor, constant: 5).isActive = true;
artDescriptionLabel.topAnchor.constraint(equalTo: artNameLabel.bottomAnchor, constant: 5).isActive = true;
artDescriptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5).isActive = true;
// in art description label don't set the height anchors so it can expand
// artist name
artistNameLabel.leadingAnchor.constraint(equalTo: artworkImageView.trailingAnchor, constant: 5).isActive = true;
artistNameLabel.topAnchor.constraint(equalTo: artDescriptionLabel.bottomAnchor, constant: 5).isActive = true;
artistNameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5).isActive = true;
artistNameLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true; // this constraint is requierd for dynamic cell
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
And if this answer not in your case please tell me .
Your tableviewcells don't know what their height is suppose to be. Which is fine.
You just have to give enough constraints so that it can figure it out. You're not doing that!
The major issue I currently see is that the artworkImageView is not constrained to top and bottom. Every vertical axiom of views needs to be constrained top to bottom.
Your first vertical axiom is just the image. It doesn't have a bottom constraint. Add that. So the tableviewcell knows how much it needs to resize itself. I strongly recommend you to see this moment of WWDC.
Also earlier in the same video at this moment it strongly recommend that you just dump your views in multiple stackviews and organize it that way. So that's also an alternative.
PS:
don't dump self. It just increases the line width with no added benefit.
Move all your non-layout related setup of your labels/images to their own instantiation. e.g.
lazy label : UILabel = {
let label = UILabel()
label.text = "John"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
No need to mention frame : .zero. Just UILabel() implies the frame is zero.
It's always best to start your UI small then keep adding more elements to it. That way debugging your layout becomes easier/smaller.

Resources