UITableViewCell doesn't use autolayout height when using insert cell into table - ios

Background
I use purelayout to programmatically create my UITableViewCells, following the instructions here, which basically states that you gotta set the top/bottom constraints on a cell, then use
self.tableView.rowHeight = UITableViewAutomaticDimension;
to get it right:
Problem
Everything works fine, except when I insert a new row into a tableView. I get this effect: https://youtu.be/eTGWsxwDAdk
To explain: as soon as I click on one of the tip cells, the table is supposed to insert a driver tip row. However you'll notice that wen i refresh the section (by clicking on a tip box), all the cells height inexplicably increases, but when i click on tip boxes again, they go back to their normal height
this is done with this code
self.tableView.beginUpdates()
self.tableView.reloadSections(IndexSet(integer:1), with: .automatic)
self.tableView.endUpdates()
this is the implementation of the cellfor row
// init table
self.tableView.register(OrderChargeTableViewCell.self,
forCellReuseIdentifier: OrderChargeTableViewCell.regularCellIdentifier)
self.tableView.register(OrderChargeTableViewCell.self,
forCellReuseIdentifier: OrderChargeTableViewCell.boldCellIdentifier)
self.tableView.estimatedRowHeight = 25
self.tableView.rowHeight = UITableViewAutomaticDimension
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: OrderChargeTableViewCell?
if (indexPath.row == filteredModel.count-1) {
cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.boldCellIdentifier,
for: indexPath) as? OrderChargeTableViewCell
} else if (indexPath.row < filteredModel.count) {
cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.regularCellIdentifier,
for: indexPath) as? OrderChargeTableViewCell
}
// add data to cell labels
return cell!
}
and this is the code for the UITableViewCell itself:
final class OrderChargeTableViewCell: UITableViewCell {
// MARK: - init
static let boldCellIdentifier = "TTOrderDetailBoldTableViewCell"
static let regularCellIdentifier = "TTOrderDetailRegularTableViewCell"
private var didSetupConstraints = false
..
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
self.keyLabel = TTRLabel()
self.valueLabel = TTRLabel()
if (reuseIdentifier == OrderChargeTableViewCell.regularCellIdentifier) {
self.isCellStyleBold = false
} else if (reuseIdentifier == OrderChargeTableViewCell.boldCellIdentifier) {
self.isCellStyleBold = true
} else {
self.isCellStyleBold = false
assertionFailure( "Attempt to create an OrderCharge cell with the wrong cell identifier: \(String(describing: reuseIdentifier))")
}
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
contentView.addSubview(valueLabel)
}
override func updateConstraints()
{
if !didSetupConstraints {
if (isCellStyleBold) {
self.applyBoldFormatting()
} else {
self.applyRegularFormatting()
}
didSetupConstraints = true
}
super.updateConstraints()
}
public func applyBoldFormatting() {
keyLabel.font = .ttrSubTitle
valueLabel.font = .ttrBody
keyLabel.autoPinEdge(.leading, to: .leading, of: contentView, withOffset: 15)
keyLabel.autoAlignAxis(.vertical, toSameAxisOf: contentView)
keyLabel.autoPinEdge(.top, to: .top, of: contentView, withOffset: 8)
keyLabel.autoPinEdge(.bottom, to: .bottom, of: contentView, withOffset: -8)
valueLabel.autoPinEdge(.trailing, to: .trailing, of: contentView, withOffset: -15)
valueLabel.autoAlignAxis(.baseline, toSameAxisOf: keyLabel)
}
public func applyRegularFormatting() {
keyLabel.font = .ttrCaptions
valueLabel.font = TTRFont.Style.standard(.h3).value
keyLabel.autoPinEdge(.leading, to: .leading, of: contentView, withOffset: 15)
keyLabel.autoAlignAxis(.vertical, toSameAxisOf: contentView)
keyLabel.autoPinEdge(.top, to: .top, of: contentView, withOffset: 6)
keyLabel.autoPinEdge(.bottom, to: .bottom, of: contentView, withOffset: -4)
valueLabel.autoPinEdge(.trailing, to: .trailing, of: contentView, withOffset: -15)
valueLabel.autoAlignAxis(.baseline, toSameAxisOf: keyLabel)
}
the driver tip row that gets inserted has the standard 44 pixel height of a cell:
whereas the other (properly formatted) cells have the 25 height:

While the StackOverflow answer you followed has a lot of up-votes, it appears you took one bullet point which was not very well explained (and may be outdated), and I think that may be what's causing your problems.
You'll find many comments / posts / articles stating you should add your constraints in updateConstraints(), however, Apple's docs also state:
Override this method to optimize changes to your constraints.
Note
It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred. For example, if you want to change a constraint in response to a button tap, make that change directly in the button’s action method.
You should only override this method when changing constraints in place is too slow, or when a view is producing a number of redundant changes.
I think you'll get much better results in what you're attempting if you add your subviews and their constraints when your cell is init'd.
Here is a simple example which has a similar layout to what you've shown. It creates a table with 2 sections - first section has a row with a "show/hide" button. When tapped, the second section will add/remove the "Driver Tip" row.
//
// InsertRemoveViewController.swift
//
// Created by Don Mag on 12/4/18.
//
import UIKit
struct MyRowData {
var title: String = ""
var value: CGFloat = 0.0
}
class OrderChargeTableViewCell: UITableViewCell {
static let boldCellIdentifier: String = "TTOrderDetailBoldTableViewCell"
static let regularCellIdentifier: String = "TTOrderDetailRegularTableViewCell"
var keyLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var valueLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(keyLabel)
contentView.addSubview(valueLabel)
let s = type(of: self).boldCellIdentifier
if self.reuseIdentifier == s {
NSLayoutConstraint.activate([
keyLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8.0),
keyLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8.0),
keyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15.0),
valueLabel.centerYAnchor.constraint(equalTo: keyLabel.centerYAnchor, constant: 0.0),
valueLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15.0),
])
keyLabel.font = UIFont.systemFont(ofSize: 15, weight: .bold)
valueLabel.font = UIFont.systemFont(ofSize: 15, weight: .bold)
} else {
NSLayoutConstraint.activate([
keyLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6.0),
keyLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4.0),
keyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15.0),
valueLabel.centerYAnchor.constraint(equalTo: keyLabel.centerYAnchor, constant: 0.0),
valueLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15.0),
])
keyLabel.font = UIFont.systemFont(ofSize: 12, weight: .bold)
valueLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
}
}
}
class TipCell: UITableViewCell {
var callBack: (() -> ())?
var theButton: UIButton = {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Tap to Show/Hide Add Tip row", for: .normal)
b.setTitleColor(.blue, for: .normal)
b.backgroundColor = .yellow
return b
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(theButton)
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
theButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
theButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor, constant: 0.0),
])
theButton.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
}
#objc func btnTapped(_ sender: Any?) -> Void {
callBack?()
}
}
class InsertRemoveViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var myData = [
MyRowData(title: "SUBTOTAL", value: 4),
MyRowData(title: "DELIVERY CHARGE", value: 1.99),
MyRowData(title: "DISCOUNT", value: -1.99),
MyRowData(title: "TOTAL", value: 4),
]
var tableView: UITableView = {
let v = UITableView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
func tipRowShowHide() {
let iPath = IndexPath(row: 3, section: 1)
if myData.count == 4 {
myData.insert(MyRowData(title: "DRIVER TIP", value: 2.0), at: 3)
tableView.insertRows(at: [iPath], with: .automatic)
} else {
myData.remove(at: 3)
tableView.deleteRows(at: [iPath], with: .automatic)
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(OrderChargeTableViewCell.self,
forCellReuseIdentifier: OrderChargeTableViewCell.regularCellIdentifier)
tableView.register(OrderChargeTableViewCell.self,
forCellReuseIdentifier: OrderChargeTableViewCell.boldCellIdentifier)
tableView.register(TipCell.self, forCellReuseIdentifier: "TipCell")
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 25
view.backgroundColor = .red
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 200.0),
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20.0),
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20.0),
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
])
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return " "
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == 0 ? 1 : myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "TipCell", for: indexPath) as! TipCell
cell.callBack = {
self.tipRowShowHide()
}
return cell
}
var cell: OrderChargeTableViewCell?
if indexPath.row == myData.count - 1 {
cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.boldCellIdentifier,
for: indexPath) as? OrderChargeTableViewCell
} else {
cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.regularCellIdentifier,
for: indexPath) as? OrderChargeTableViewCell
}
cell?.keyLabel.text = myData[indexPath.row].title
let val = myData[indexPath.row].value
cell?.valueLabel.text = String(format: "%0.02f USD", val)
return cell!
}
}
This is the result:

this is a brute force solution, one that i'm not proud of at all, but it's here for reference (won't mark it as correct answer):
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let orderChargesSection = self.getOrderChargesSection()
switch indexPath.section {
case orderChargesSection:
return self.getCellHeightForOrderCharges(row: indexPath.row)
default:
return UITableViewAutomaticDimension
}
}
private func getCellHeightForOrderCharges(row: Int) -> CGFloat {
let numRows = self.tableView(self.tableView, numberOfRowsInSection: self.getOrderChargesSection())
if (row == numRows - 1) {
return UITableViewAutomaticDimension
} else {
return 25.5
}
}

After beginUpdates / endUpdate and all of the inserts do this:
DispatchQueue.main.async {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}

Related

How to dynamically resize table view header?

When people click on Load More, I want to dynamically resize the table view header. What happens is that new content gets added to the table view header when people click on load more, so that height changes. I don't know the new height beforehand.
How can I do this? What I'm doing now is when people click on "Load More", I execute the code below.
#objc func expandDesc(sender: UIButton) {
loadMoreDesc = !loadMoreDesc
tableView.reloadData()
}
What can I add to the code above to dynamically resize the table view header?
Just use constraints, You can animate them. You don't have to reload whole table view for that. You can keep weak reference to header and change it state, If more is pressed, animate constraints, If it is text You want to fit, use StackView and just add/remove UILabel to stack view and it should work perfectly.
Auto-layout does not automatically update the size of a table header view, so we need to do it "manually."
We can use this extension to help:
extension UITableView {
func sizeHeaderToFit() {
guard let headerView = tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableHeaderView = headerView
}
}
}
Now, when we update the content of the table header view - which would cause its height to change - we can call .sizeHeadrToFit()
Here's a complete example:
Simple multiline cell - cyan label
class MultilineCell: UITableViewCell {
let label: UILabel = {
let v = UILabel()
v.numberOfLines = 0
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()
}
func commonInit() -> Void {
label.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(label)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
// constrain label to the cell's margins guide
label.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
label.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
label.backgroundColor = .cyan
}
}
Multiline view for table header - yellow label in a red view
class MyTableHeaderView: UIView {
let label: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
let g = self.layoutMarginsGuide
NSLayoutConstraint.activate([
// constrain label to the self's margins guide
label.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
])
// this avoids auto-layout complaints
let c = label.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
backgroundColor = .red
label.backgroundColor = .yellow
}
}
Example table view controller
class DynamicHeaderTableViewController: UITableViewController {
var theData: [String] = []
let myHeaderView = MyTableHeaderView()
override func viewDidLoad() {
super.viewDidLoad()
// 10 rows with 2-to-5 lines per row
for i in 1...10 {
let s = "This is row \(i)"
let n = Int.random(in: 1...4)
let a = (1...n).map { "Line \($0)" }
theData.append(s + "\n" + a.joined(separator: "\n"))
}
tableView.register(MultilineCell.self, forCellReuseIdentifier: "cell")
myHeaderView.label.text = "Select a row..."
tableView.tableHeaderView = myHeaderView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.sizeHeaderToFit()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MultilineCell
c.label.text = theData[indexPath.row]
return c
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
myHeaderView.label.text = theData[indexPath.row]
tableView.sizeHeaderToFit()
}
}
The above code will generate 10 rows with 2 to 5 lines per row. On didSelectRowAt we'll update the table view header with the text from the row.
Result when first launched:
After selecting "Row 2":
After selecting "Row 3":

how to popup tableView from bottom of screen?

in my project I want that after presenting viewController, tableView popup from bottom of screen until showing all the content of tableView. the UITableViewCell height is dynamic and I'm using swift language and NSLayoutConstraint in my project. how can I do this? it's only showing some part of tableView.
this is my codes:
class myViewController: UIViewController {
var tableView: UITableView!
var tableViewTopLayoutConstraint, tableViewHeightLayoutConstraint: NSLayoutConstraint!
private var isFirstTime: Bool = true
override func viewDidLayoutSubviews() {
print("this is tableView frame Height:\(tableView.frame.height)")
print("this is tableView content Height:\(tableView.contentSize.height)")
if tableView.contentSize.height != 0 && isFirstTime {
tableViewHeightLayoutConstraint.constant = tableView.contentSize.height
showContainerView(-tableView.contentSize.height)
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
tableView = UITableView()
tableView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
tableView.layer.cornerRadius = 20
tableView.register(SomeUITableViewCell.self, forCellReuseIdentifier: SomeUITableViewCell.identifier)
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none
tableView.backgroundColor = .clear
tableView.tableFooterView = nil
tableView.isScrollEnabled = false
tableView.delegate = self
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableViewTopLayoutConstraint = tableView.topAnchor.constraint(equalTo: view.bottomAnchor, constant: 0)
tableViewTopLayoutConstraint.isActive = true
tableViewHeightLayoutConstraint = tableView.heightAnchor.constraint(equalToConstant: 0)
tableViewHeightLayoutConstraint.isActive = true
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
private func showContainerView(_ viewHeight: CGFloat) {
isFirstTime = false
tableViewTopLayoutConstraint.constant = viewHeight
UIView.animate(withDuration: 0.5) { [weak self] in
self?.view.layoutIfNeeded()
}
}
}
extension myViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: SomeUITableViewCell.identifier, for: indexPath) as! SomeUITableViewCell
cell.setCell()
return cell
}
}
class SomeUITableViewCell: UITableViewCell {
static let identifier = "SomeUITableViewCellId"
private var cardOrDepositNumberLabel: UILabel!
private var cardOrDepositOwnerLabel: UILabel!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
cardOrDepositNumberLabel = UILabel()
cardOrDepositNumberLabel.numberOfLines = 0
cardOrDepositNumberLabel.backgroundColor = .red
cardOrDepositNumberLabel.textAlignment = .right
cardOrDepositNumberLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(cardOrDepositNumberLabel)
NSLayoutConstraint.activate([
cardOrDepositNumberLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
cardOrDepositNumberLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
cardOrDepositNumberLabel.topAnchor.constraint(equalTo: topAnchor),
])
cardOrDepositOwnerLabel = UILabel()
cardOrDepositOwnerLabel.numberOfLines = 0
cardOrDepositOwnerLabel.textAlignment = .right
cardOrDepositOwnerLabel.backgroundColor = .blue
cardOrDepositOwnerLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(cardOrDepositOwnerLabel)
NSLayoutConstraint.activate([
cardOrDepositOwnerLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
cardOrDepositOwnerLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
cardOrDepositOwnerLabel.topAnchor.constraint(equalTo: cardOrDepositNumberLabel.bottomAnchor),
cardOrDepositOwnerLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setCell() {
cardOrDepositNumberLabel.text = "some data some data some data"
cardOrDepositOwnerLabel.text = "some data some data some data some data some data some data some data some data some data some data"
}
}
Problem is here
tableViewHeightLayoutConstraint = tableView.heightAnchor.constraint(equalToConstant: 300)
set an intial no zero value and do what is inside viewDidLayoutSubviews in viewDidAppear preferably after a delay to give some time to the table to calculate it's actualy contentSize

NSLayoutConstraints to limit label width

I am struggling to set the necessary constraints to limit the width of a label to the leading anchor of a button in a tableview cell.
Desired Result
Label to the left, button to the right. Label wraps before button if needed.
Current Result
The label is pulling the red button to the left.
Code
[button.centerYAnchor constraintEqualToAnchor:cell.contentView.centerYAnchor constant:0].active = YES;
[button.trailingAnchor constraintEqualToAnchor:cell.contentView.trailingAnchor constant:-5].active = YES;
[label.leadingAnchor constraintEqualToAnchor:cell.contentView.leadingAnchor constant:10].active = YES;
[label.trailingAnchor constraintEqualToAnchor:button.leadingAnchor constant:0].active = YES;
I have tried various things like adding another label constraint to the right of the cell but that squeezed the button (I tried to fix by setting its compression resistance priority but that had no effect)
What constraints do I need to achieve the desired result please?
Just change
[label.trailingAnchor constraintEqualToAnchor:button.leadingAnchor constant:0].active = YES;
into
[label.trailingAnchor constraintLessThanOrEqualToAnchor:button.leadingAnchor constant:0].active = YES;
import UIKit
final class ViewController: UIViewController {
lazy var tableView: UITableView = {
let val = UITableView(frame: self.view.bounds, style: .plain)
val.dataSource = self
val.delegate = self
val.estimatedRowHeight = 44
val.register(MyCell.self, forCellReuseIdentifier: "MyCell")
return val
}()
let data: [String] = [
"LayoutDemo",
"LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo",
"LayoutDemoLayoutDemoLayoutDemoLayoutDemo"
]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as? MyCell else { fatalError() }
cell.dataText = data[indexPath.row]
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
class MyCell: UITableViewCell {
var dataText: String? {
didSet {
label.text = dataText
}
}
lazy var label: UILabel = {
let val = UILabel(frame: .zero)
val.translatesAutoresizingMaskIntoConstraints = false
val.numberOfLines = 0
val.backgroundColor = .gray
return val
}()
lazy var button: UIButton = {
let val = UIButton(type: .custom)
val.translatesAutoresizingMaskIntoConstraints = false
val.backgroundColor = .red
return val
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(label)
contentView.addSubview(button)
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
button.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5),
button.heightAnchor.constraint(equalToConstant: 20),
button.widthAnchor.constraint(equalToConstant: 20),
label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
label.trailingAnchor.constraint(lessThanOrEqualTo: button.leadingAnchor, constant: -10)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

How to expand and collapse tableview cells with dynamic height programmatically?

Here is a video of my problem: https://imgur.com/a/qvGSLfD
My custom cell has 2 labels: one main label, and a subtitle label, both of which are constrained to the cell's contentView layoutMarginsGuide in all directions.
First of all, the whole expand collapse mechanism seems a bit clunky since I'm activating and deactivating constraints and then reloading the indexPaths.
I've tried this approach: When I select the cell, I deactivate the constraints that are responsible for setting only one label and activate the constraints for both, and when it collapses I do the opposite. I feel like this is not a good approach but I was not able to find anything that supported dynamic cell heights.
Basically, I could’ve done something like this:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == selectedRowIndex {
return 200 //Expanded
}
return tableView.rowHeight //Not expanded
}
but I can't just return the same number for cells that are sized differently.
Here is my tableView code in my viewController class:
var tableView: UITableView = {
let tv = UITableView(frame: .zero)
tv.register(CustomCell.self, forCellReuseIdentifier: CustomCell.reuseIdentifier())
tv.translatesAutoresizingMaskIntoConstraints = false
tv.rowHeight = UITableView.automaticDimension
tv.estimatedRowHeight = 60.0
tv.estimatedSectionHeaderHeight = 0
tv.estimatedSectionFooterHeight = 0
tv.showsVerticalScrollIndicator = false
tv.tableFooterView = UIView()
tv.alwaysBounceVertical = true
tv.decelerationRate = .fast
tv.bounces = false
tv.dataSource = self
tv.delegate = self
return tv
}()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomCell.reuseIdentifier(), for: indexPath) as! CustomCell
cell.bounds = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 99999)
cell.contentView.bounds = cell.bounds
cell.layoutIfNeeded()
cell.wordLabel.preferredMaxLayoutWidth = cell.wordLabel.frame.width
cell.descriptionLabel.preferredMaxLayoutWidth = cell.descriptionLabel.frame.width
//customize this later
cell.backgroundColor = .white
cell.set(content: datasource[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! CustomCell
cell.toggle()
tableView.reloadRows(at: [indexPath], with: .automatic)
}
Here are the relevant custom Cell functions:
var isExpanded: Bool!
private var singleLabelConstraints: [NSLayoutConstraint]!
private var doubleLabelConstraints: [NSLayoutConstraint]!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupLabels()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLabels()
}
func toggle() {
isExpanded = !isExpanded
if isExpanded == false {
print("collapsed")
self.descriptionLabel.isHidden = true
NSLayoutConstraint.activate(singleLabelConstraints)
NSLayoutConstraint.deactivate(doubleLabelConstraints)
} else if isExpanded == true {
print("expanded")
self.descriptionLabel.isHidden = false
NSLayoutConstraint.deactivate(singleLabelConstraints)
NSLayoutConstraint.activate(doubleLabelConstraints)
}
}
func setupLabels() {
isExpanded = false
descriptionLabel.isHidden = true
self.contentView.addSubview(wordLabel)
self.contentView.addSubview(descriptionLabel)
singleLabelConstraints = [
wordLabel.leadingAnchor.constraint(
equalTo: self.contentView.layoutMarginsGuide.leadingAnchor,
constant: labelInsets.left
),
wordLabel.topAnchor.constraint(
equalTo: self.contentView.layoutMarginsGuide.topAnchor,
constant: labelInsets.top
),
wordLabel.trailingAnchor.constraint(
equalTo: self.contentView.layoutMarginsGuide.trailingAnchor,
constant: labelInsets.right
),
wordLabel.bottomAnchor.constraint(
equalTo: self.contentView.layoutMarginsGuide.bottomAnchor,
constant: labelInsets.bottom
)
]
doubleLabelConstraints = [
wordLabel.leadingAnchor.constraint(
equalTo: self.contentView.layoutMarginsGuide.leadingAnchor,
constant: labelInsets.left
),
wordLabel.topAnchor.constraint(
equalTo: self.contentView.layoutMarginsGuide.topAnchor,
constant: labelInsets.top
),
wordLabel.trailingAnchor.constraint(
equalTo: self.contentView.layoutMarginsGuide.trailingAnchor,
constant: labelInsets.right
),
descriptionLabel.leadingAnchor.constraint(
equalTo: self.contentView.layoutMarginsGuide.leadingAnchor,
constant: labelInsets.left
),
descriptionLabel.topAnchor.constraint(
equalTo: self.wordLabel.bottomAnchor,
constant: labelInsets.top
),
descriptionLabel.trailingAnchor.constraint(
equalTo: self.contentView.layoutMarginsGuide.trailingAnchor,
constant: labelInsets.right
),
descriptionLabel.bottomAnchor.constraint(
equalTo: self.contentView.layoutMarginsGuide.bottomAnchor,
constant: labelInsets.bottom
)
]
NSLayoutConstraint.activate(singleLabelConstraints)
}
I expected the transition to be smoother and I want something that can easily toggle the cell while keeping the main label in place and just show then subtitle label.
You can use UIStackView for expand and collapse tableview. You can hide and show the description label when tableview cell is selected.
class ViewController: UIViewController {
var tableView: UITableView = {
let tv = UITableView(frame: .zero)
tv.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
tv.translatesAutoresizingMaskIntoConstraints = false
tv.rowHeight = UITableView.automaticDimension
tv.estimatedRowHeight = 100.0
tv.estimatedSectionHeaderHeight = 0
tv.estimatedSectionFooterHeight = 0
tv.showsVerticalScrollIndicator = false
tv.tableFooterView = UIView()
tv.alwaysBounceVertical = true
tv.decelerationRate = .fast
tv.bounces = false
return tv
}()
var selectedCell:IndexPath?
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(tableView)
tableView.dataSource = self
tableView.delegate = self
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[tableView]|", options: [], metrics: nil, views: ["tableView":tableView]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[tableView]|", options: [], metrics: nil, views: ["tableView":tableView]))
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell") as? CustomCell ?? CustomCell()
if let selectedCell = selectedCell, selectedCell == indexPath {
cell.descriptionLabel.isHidden = false
} else {
cell.descriptionLabel.isHidden = true
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedCell = indexPath
tableView.reloadData()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
class CustomCell: UITableViewCell {
let stackView = UIStackView()
let wordLabel = UILabel()
let descriptionLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupLabels()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLabels()
}
func setupLabels() {
selectionStyle = .none
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 5
stackView.alignment = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(stackView)
wordLabel.translatesAutoresizingMaskIntoConstraints = false
wordLabel.text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lor"
wordLabel.numberOfLines = 0
wordLabel.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(wordLabel)
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
descriptionLabel.text = "It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
descriptionLabel.numberOfLines = 0
descriptionLabel.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(descriptionLabel)
wordLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
descriptionLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
stackView.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor,constant: 10).isActive = true
stackView.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor,constant: 10).isActive = true
stackView.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor,constant: 10).isActive = true
stackView.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor,constant: 10).isActive = true
}
}
Try this:
tableView.beginUpdates()
tableView.reloadRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
hello there you can overcome the problem by going through this link below,
http://vasundharavision.com/blog/ios/expandable-tableview-without-third-party
i hope it works for you.

UITableViewAutomaticDimension works not as expected. Swift

After reading Ray Wenderlich guide for "Self-sizing Table View Cells" as well as this question and answers to it, I've decided to ask all of you for a help.
Have a programmically created cell:
import UIKit
class NotesCell: UITableViewCell {
lazy private var cellCaption: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 20, weight: UIFont.Weight.medium)
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
return label
}()
func configure(with note: NotesModel) {
cellCaption.text = note.name
contentView.addSubview(cellCaption)
}
override func layoutSubviews() {
super.layoutSubviews()
NSLayoutConstraint.activate([
cellCaption.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
cellCaption.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
cellCaption.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
// cellCaption.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
cellCaption.bottomAnchor.constraint(greaterThanOrEqualTo: contentView.bottomAnchor, constant: -8)
])
// cellCaption.sizeToFit()
// cellCaption.layoutIfNeeded()
}
}
The table view controller uses UITableViewAutomaticDimension in the delegate methods:
extension NotesTableViewController {
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
}
As a result, the longest caption is indicated fully, but the cell anyway has the same height as all other.
Some update!
I've already tried to put into viewDidLoad() following code:
tableView.rowHeight = 44
tableView.estimatedRowHeight = UITableViewAutomaticDimension
with enabling delegate methods and disabling them as well. The result is the same :(
I would recommend not to use the Delegate-Methods for your needs.
Just try setting this in your viewDidLoad:
self.tableView.rowHeight = UITableViewAutomaticDimension;
// set estimatedRowHeight to whatever is the fallBack rowHeight
self.tableView.estimatedRowHeight = 44.0;
This always works for me. Let me know if it helps :)
You're doing a number of things wrong, but the main point is your use of greaterThanOrEqualTo:.
Instead, it should be:
cellCaption.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
Also, your current code is adding a new label as a subview every time you set the text. Cells are reused, so you only want to add the label when the cell is created.
Next, the correct properties for the table are:
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44
Put those two lines in viewDidLoad() of your table view controller, and do not implement heightForRowAt or estimatedHeightForRowAt functions. You can delete your extension entirely.
And finally, you only need to set the constraints once. Definitely NOT in layoutSubviews().
Here's a full example:
//
// NotesTableViewController.swift
//
// Created by Don Mag on 8/29/18.
//
import UIKit
class NotesModel: NSObject {
var name: String = ""
}
class NotesCell: UITableViewCell {
lazy private var cellCaption: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 20, weight: UIFont.Weight.medium)
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
return label
}()
func configure(with note: NotesModel) {
cellCaption.text = note.name
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(cellCaption)
NSLayoutConstraint.activate([
cellCaption.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
cellCaption.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
cellCaption.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
cellCaption.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
])
}
}
class NotesTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 8
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotesCell", for: indexPath) as! NotesCell
let m = NotesModel()
if indexPath.row == 3 {
m.name = "This is a very long caption. It will demonstrate how the cell height is auto-sized when the text is long enough to wrap to multiple lines."
} else {
m.name = "Caption \(indexPath.row)"
}
cell.configure(with: m)
return cell
}
}
Result:
Update the following line:
cellCaption.bottomAnchor.constraint(greaterThanOrEqualTo: contentView.bottomAnchor, constant: -8)
])
to:
cellCaption.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)
])
and add following in viewDidLoad:
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0;
Following steps may solve your problem:
1) set top, bottom, leading and trailing constraints for the UILabel in the cell like below:
2) configure tableview:
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0;
In Swift 5:
func configureTableView() {
myTableView.rowHeight = UITableView.automaticDimension
myTableView.estimatedRowHeight = 44
}
Keep in mind that if the .estimatedRowHeight is not correct, Swift will do the math for you. Finally, call this method in the viewDidLoad()

Resources