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":
Related
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
I am trying to build a TableView programmatically, but I cannot get a basic standard label to display; all I see is basic empty cells. Here's my code:
TableView Cell:
class TableCell: UITableViewCell {
let cellView: UIView = {
let view = UIView()
view.backgroundColor = .systemRed
return view
}()
let labelView: UILabel = {
let label = UILabel()
label.text = "Cell 1"
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
addSubview(cellView)
NSLayoutConstraint.activate([
cellView.topAnchor.constraint(equalTo: topAnchor),
cellView.bottomAnchor.constraint(equalTo: bottomAnchor),
cellView.leadingAnchor.constraint(equalTo: leadingAnchor),
cellView.trailingAnchor.constraint(equalTo: trailingAnchor)])
cellView.addSubview(labelView)
}
}
Data Source:
class TableDataSource: NSObject, UITableViewDataSource {
let cellID = "cell"
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! TableCell
return cell
}
}
And this is the VC:
class TableViewController: UITableViewController {
let dataSource = TableDataSource()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(TableCell.self, forCellReuseIdentifier: dataSource.cellID)
tableView.dataSource = dataSource
}
}
I am trying to keep the code as basic as possible for future references. I've set various breakpoints to see what could go wrong, but they all check out. Could it be the constraints that are wrong?
Any help is appreciated.
I see several errors in your cell.
Add subviews to contentView, not directly to cell:
contentView.addSubview(cellView)
cellView.addSubview(labelView)
The same is necessary for constraints:
NSLayoutConstraint.activate([
cellView.topAnchor.constraint(equalTo: contentView.topAnchor),
cellView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
cellView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
cellView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
])
Views created in code need to set translatesAutoresizingMaskIntoConstraints = false,
let cellView: UIView = {
let view = UIView()
view.backgroundColor = .systemRed
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let labelView: UILabel = {
let label = UILabel()
label.text = "Cell 1"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
There are no constraints for your label.
Your constraints don't work, because you need to change translatesAutoresizingMaskIntoConstraints for cellView in your setup():
func setup() {
addSubview(cellView)
cellView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
cellView.topAnchor.constraint(equalTo: topAnchor),
cellView.bottomAnchor.constraint(equalTo: bottomAnchor),
cellView.leadingAnchor.constraint(equalTo: leadingAnchor),
cellView.trailingAnchor.constraint(equalTo: trailingAnchor)])
cellView.addSubview(labelView)
}
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()
}
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()
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
}