UITableViewAutomaticDimension works not as expected. Swift - ios

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()

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":

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")
}
}

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

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()
}

Set table view height based on cell height for x visible cells

I want to adjust my table view height so that it shows only x cells at a time. The cell's layout is configurable so I don't know its height beforehand.
For simplicity's sake, I've tried to set up the simplest example of a cell with two labels with fixed text stacked vertically.
MyCustomViewCell.swift
class MyCustomViewCell: UITableViewCell {
let label: UILabel
let label2: UILabel
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
label = UILabel()
label2 = UILabel()
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.contentView.addSubview(label)
self.contentView.addSubview(label2)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor).isActive = true
label2.translatesAutoresizingMaskIntoConstraints = false
label2.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
label2.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor).isActive = true
label2.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true
label2.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor).isActive = true
label.text = "string 1"
label2.text = "string 2"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I have managed to achieve the desired behavior by setting the height constraint (with a lower than 1000 priority) when initializing the table view and then setting the actual height on cellForRowAtIndexPath based on the cell's frame and the amount of cells I wanted to be visible (in this case, 5).
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let cellIdentifier = "cellIdentifier"
override func viewDidLoad() {
super.viewDidLoad()
let tableView = UITableView(frame: .zero, style: .plain)
setupTableViewConstraints(tableView: tableView)
tableView.register(MyCustomViewCell.self, forCellReuseIdentifier: cellIdentifier)
tableView.dataSource = self
tableView.delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setupTableViewConstraints(tableView: UITableView) {
self.view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 50).isActive = true
let heightConstraint = tableView.heightAnchor.constraint(equalToConstant: 300)
heightConstraint.priority = 900
heightConstraint.isActive = true
tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10).isActive = true
tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10).isActive = true
tableView.layer.borderColor = UIColor.black.cgColor
tableView.layer.borderWidth = 0.5
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10000
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath as IndexPath) as! MyCustomViewCell
tableView.heightAnchor.constraint(equalToConstant: cell.frame.height*5).isActive = true
return cell
}
}
However, this approach doesn't seem right to me. All the way from setting a "dummy" height only to be adjusted later to setting the table view height every time cellForRowAtIndexPath is executed.
Can anyone point me in the direction of a more elegant solution?
In your example you are updating the height of your tableview every time it asks you to load a cell.
While this would work, it seems a bit overkill knowing that this method is constantly called by the tableview even before the cells are visible.
Why not wait until the tableview has finished loading and only then update the height?
You could do this in willDisplayCell instead which is trigged only when the cells are about to be displayed or perhaps with a completion closure like so:
self.tableView.reloadData()
DispatchQueue.main.async {
// This closure is only called when the data has been reloaded
// and thus will enable you to calculate the final content height
}

How to add padding inside a UITableViewCell with self-sizing cells?

I'm looking at the Instagram comment table view, and each cell self sizes depending on the length of the comment with some kind of padding on the top and bottom. Now I tried doing something similar, except I have a problem self-sizing the table view cell. I try to add constraints to achieve the padding effect, but the text overlaps the next cell.
I've tried tableView.contentInset but it didn't change anything.
Here's what I want:
Here's what ends up happening:
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 130.0
tableView.tableFooterView = UIView()
tableView.separatorInset.left = 50
tableView.registerClass(CommentCellView.self, forCellReuseIdentifier: cellid)
tableView.rowHeight = UITableViewAutomaticDimension
tableView.contentInset = UIEdgeInsetsMake(15, 15, 15, 15)
}
override func viewDidAppear(animated: Bool) {
tableView.reloadData()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellid, forIndexPath: indexPath) as! CommentCellView
cell.layoutIfNeeded()
return cell
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 60.0
}
}
class CommentCellView: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier)
contentView.addSubview(commentLabel)
commentLabel.leftAnchor.constraintEqualToAnchor(contentView.leftAnchor).active = true
commentLabel.rightAnchor.constraintEqualToAnchor(contentView.rightAnchor).active = true
commentLabel.topAnchor.constraintEqualToAnchor(contentView.topAnchor, constant: 10).active = true
commentLabel.bottomAnchor.constraintEqualToAnchor(contentView.bottomAnchor, constant: 10).active = true
self.contentView.layoutMargins = UIEdgeInsetsMake(15, 15, 15, 15)
}
Your constraints didn't look correct to me. You should set a negative value for right and bottom constraints as contentView's bounds are greater than the label's:
commentLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10).isActive = true
Here is the corrected version of your code:
commentLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10).isActive = true
commentLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10).isActive = true
commentLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10).isActive = true
commentLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10).isActive = true
You can define a helper function for later use like follows:
func inset(view: UIView, insets: UIEdgeInsets) {
if let superview = view.superview {
view.translatesAutoresizingMaskIntoConstraints = false
view.leftAnchor.constraint(equalTo: superview.leftAnchor, constant: insets.left).isActive = true
view.rightAnchor.constraint(equalTo: superview.rightAnchor, constant: -insets.right).isActive = true
view.topAnchor.constraint(equalTo: superview.topAnchor, constant: insets.top).isActive = true
view.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: -insets.bottom).isActive = true
}
}
--
inset(commentLabel, insets: UIEdgeInsetsMake(10, 10, 10, 10))

Resources