I've prepared UITableViewCell in nib file with all constraints from top to bottom. Calculating row height in table view seems to work.
But if I want to activate/deactivate some other constraints in the cell based on some row data condition then I have problem because it seems like the height is not calculated then. What is the problem? In the following example TableRowDM.hidden means should subtitle be hidden or not. I'm hidding this UILabel by activating not activated constraint (height with constant = 0) and setting vertical gap between titleLabel and subtitleLabel to 0.
struct TableRowDM {
let title: String
let subTitle: String
let hidden: Bool
}
let cellId: String = "CellView"
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let rowData: [TableRowDM] = [
TableRowDM(title: "Row 1", subTitle: "Short title 1th row", hidden: false),
TableRowDM(title: "Row 2", subTitle: "Short title 2th row", hidden: true),
TableRowDM(title: "Row 3", subTitle: "Very long text in subtitle at 3th row to test text wrapping and growing subtitle height", hidden: false),
TableRowDM(title: "Row 4", subTitle: "Long text in subtitle at 4th row", hidden: false),
TableRowDM(title: "Row 5", subTitle: "Long text in subtitle at 5th row", hidden: true),
TableRowDM(title: "Row 6", subTitle: "Long text in subtitle at 6th row", hidden: false),
]
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 50
tableView.tableFooterView = UIView(frame: CGRect.zero)
tableView.register(UINib(nibName: cellId, bundle: nil), forCellReuseIdentifier: cellId)
tableView.delegate = self
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! CellView
let row: TableRowDM = self.rowData[indexPath.row]
cell.title.text = row.title
cell.subtitle.text = row.subTitle
if row.hidden {
// hide subtitle
cell.subtitleHeight.isActive = true
cell.titleToSubtitleGap.constant = 0
} else {
// show subtitle
cell.subtitleHeight.isActive = false
cell.titleToSubtitleGap.constant = 16
}
cell.setNeedsLayout()
cell.layoutIfNeeded()
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rowData.count
}
}
It seems that this is not working and subtitleLabel is not hiding and row height is always the same. When contents of subtitleLabel and titleLabel changes then the height is adjusted to the text inside those UIViews but when I try to manipulate with constraints then it is not working. Any help? What Am I doing wrong? The CellView is very simple view with such subviews
-----------------
|
- titleLabel ----
|
| <- titleToSubtitleGap
|
- subtitleLabel -
|
-----------------
I found a solution for my question. It is not documented anywhere but it works.
What is important. If You are supporting landscape/portrait orientations then it is important to set right priorities in NSLayoutConstraints that defines vertical unbroken chain of constraints and views (with defined heights) as stated here - they wrote:
Next, lay out the table view cell’s content within the cell’s content
view. To define the cell’s height, you need an unbroken chain of
constraints and views (with defined heights) to fill the area between
the content view’s top edge and its bottom edge. If your views have
intrinsic content heights, the system uses those values. If not, you
must add the appropriate height constraints, either to the views or to
the content view itself.
What they didn't wrote is that all those vertical constraints that define finally the height of the table cell contentView must have priorities less than required (1000) if You need to support many device orientations. I set in my examples those priorities to High (750).
Next it is important to make changes with those constraints in updateConstraints method. So if You are designing a UIView and when some property of this view need to change constraints then You need to do it something like this in Your view so updateConstraints method will be called in right moment. After setting the property new value call needsUpdateConstraints() so AutoLayout will know that constraints in our UIView changed.
import UIKit
class ListItemTableViewCell: UITableViewCell {
#IBOutlet weak var subtitleToTitleGap: NSLayoutConstraint!
#IBOutlet weak var subtitleHeightZero: NSLayoutConstraint!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var subtitleLabel: UILabel!
public var isSubtitleHidden: Bool {
get {
return self.subtitleLabel.isHidden
}
set {
// Here we are changing property that change layout in our view
self.subtitleLabel.isHidden = newValue
self.needsUpdateConstraints() // Here we are telling AutoLayout to recalculate constraints in `updateConstraints` method.
}
}
#if !TARGET_INTERFACE_BUILDER
// We need to block this code from running in Interface Builder
override func updateConstraints() {
if self.isSubtitleHidden {
// Hide Subtitle
self.subtitleToTitleGap.constant = 0.0
self.subtitleHeightZero.autoInstall() // This makes subtitle height to be 0
} else {
// Show Subtitle
self.subtitleToTitleGap.constant = 2.0
self.subtitleHeightZero.autoRemove() // This will make subtitle height to be self calculated
}
super.updateConstraints() // don't forget to call this after You finished constraints changes
}
#endif
}
And the most important and it was the most difficult for me to find out is to hide implementation of updateConstraints from interface builder because it was causing IB to crash from time to time in xib editor. Look above for this example. The method is wrapped in #if preprocessor macro. So I think thats it. Good Luck!
Related
As the title says, I'm trying to display the following layout:
As you see, the dynamic stack view is a container where content is added dynamically. This content is variable and is decided on run time. Basically, it can be webviews (with variable content inside), ImageViews (with variable height), and videos (this view would have a fixed view).
I configured the CellView with automatic row height, and provided an estimated row height, both in code and in Xcode. Then on the tableView_cellForRow at the method of the ViewController, the cell is dequeued and the cell is rendered with content.
During this setup process, the different labels and views are filled with content, and the dynamic container too. The webviews are added to the stackview with the following code:
var webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.scrollView.isScrollEnabled = false
webView.navigationDelegate = myNavigationDelegate
webView = addContentToWebView(content, webView)
container.addArrangedSubview(webView)
I'm testing this with only a webview inside the stackview and having already problems with the height of the row.
The webview is rendered correctly inside the stackview, but not completely (the webview was bigger as the estimated rowheight). I used the navigation delegate to calculate the height of the added webview and resize the StackContainer accordingly, with the following code:
webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
let h = height as! CGFloat
print("Height 3 is \(h)")
self.dynamicContainerHeightContraint.constant = h
})
}
})
And indeed, the stackcontainer is resized and expanded to match the height of the webview that is inside.
But the row remains with the same estimated height, and if the webview is very big in height, then all the other views disappear (they are pushed outside the bounds of the row.
Is there a way to tell the row to autoresize and adapt to its contents? Or maybe I'm using the false approach?
I suppose the problem is that the height of the views added to the stackview is not known in advance, but I was expecting a way to tell the row to recalculate its height after adding all the needed stuff inside...
Thank you in advance.
Table views do not automatically redraw their cells when a cell's content changes.
Since you are changing the constant of your cell's dynamicContainerHeightContraint after the cell has been rendered (your web view's page load is asynchronous), the table does not auto-update -- as you've seen.
To fix this, you can add a "callback" closure to your cell, which will let the cell tell the controller to recalculate the layout.
Here is a simple example to demonstrate.
The cell has a single label... it has a "label height constraint" var that initially sets the height of the label to 30.
For the 3rd row, we'll set a 3-second timer to simulate the delayed page load in your web view. After 3 seconds, the cell's code will change the height constant to 80.
Here's how it looks to start:
Without the callback closure, here's how it looks after 3 seconds:
With the callback closure, here's how it looks after 3 seconds:
And here's the sample code.
DelayedCell UITableViewCell class
class DelayedCell: UITableViewCell {
let myLabel = UILabel()
var heightConstraint: NSLayoutConstraint!
// closure to tell the controller our content changed height
var callback: (() -> ())?
var timer: Timer?
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 {
contentView.clipsToBounds = true
myLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(myLabel)
let g = contentView.layoutMarginsGuide
// we'll change this dynamically
heightConstraint = myLabel.heightAnchor.constraint(equalToConstant: 30.0)
// use bottom anchor with Prioirty: 999 to avoid auto-layout complaints
let bc = myLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor)
bc.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
// constrain label to all 4 sides
myLabel.topAnchor.constraint(equalTo: g.topAnchor),
myLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
myLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// activate bottom and height constraints
bc,
heightConstraint,
])
}
func fillData(_ str: String, testTimer: Bool) -> Void {
myLabel.text = str
// so we can see the label frame
// green if we're testing the timer in this cell
// otherwise yellow
myLabel.backgroundColor = testTimer ? .green : .yellow
if testTimer {
// trigger a timer in 3 seconds to change the height of the label
// simulating the delayed load of the web view
timer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(self.heightChanged), userInfo: nil, repeats: false)
}
}
#objc func heightChanged() -> Void {
// change the height constraint
heightConstraint.constant = 80
myLabel.text = "Height changed to 80"
// run this example first with the next line commented
// then run it again but un-comment the next line
// tell the controller we need to update
//callback?()
}
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
timer?.invalidate()
}
}
}
DelayTestTableViewController UITableViewController class
class DelayTestTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(DelayedCell.self, forCellReuseIdentifier: "cell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DelayedCell
// we'll test the delayed content height change for row 2
let bTest = indexPath.row == 2
cell.fillData("Row \(indexPath.row)", testTimer: bTest)
// set the callback closure
cell.callback = { [weak tableView] in
guard let tv = tableView else { return }
// this will tell the tableView to recalculate row heights
// without reloading the cells
tv.performBatchUpdates(nil, completion: nil)
}
return cell
}
}
In your code, you would make the closure callback after this line:
self.dynamicContainerHeightContraint.constant = h
Here is the situation. A title label, a "read more" button and a content label in a stackView. And the stackView is in a cell, set Auto-Layout. The height of tableView's cell is set to AutoDimension. When I tap button, content label will show or hide.
The button's action method is
#IBAction func readMore(_ sender: Any) {
tableView.performBatchUpdates({
self.contentLabel.isHidden.toggle()
}, completion: nil)
}
Here is the result in slow animations:
As you can see, when the content is going to show, line 2 is presented firstly, i.e. content is presented from the center. When the content is going to hide, the content label is hidden instantly, and button is stretched to the frame that content label had before hiding. This animation is strange.
Furthermore, if I set the stackView's spacing to 10, the case becomes worse. The title label was also affected:
I adjusted everything I can,stackView's distribution, three subViews' content mode and content hugging/compression priority. I can't find an appropriate way to fix it.
Here is the ideal result:
I achieved it by a little tricky way:
#IBAction func readMore(_ sender: Any) {
tableView.performBatchUpdates({
UIView.animate(withDuration: 0.3) { // It must be 0.3
self.contentLabel.isHidden.toggle()
}
}, completion: nil)
}
I'm not sure this is the most appropriate way to fix it. So I want to know why this weird animation happens and if there is a more appropriate way to fix it. Thanks!
Animating to hide/reveal multi-line labels can be problematic, particularly when used in a stack view.
If you give it a try, you'll find that even outside of a table view cell - just the stack view in a view - you will see the same issue when toggling the .isHidden property of the label. This is due to the fact that UILabel vertically centers its text.
Here is another approach, which doesn't use a stack view (background colors for clarity):
Top Label is set to 1 line; Read More is a normal button, Bottom Label is set to 0 lines.
You will notice the pink rectangle. That is a UIView which I've named ShimView - more about that shortly.
The Top Label is constrained Top: 4, Leading: 8, Trailing: 8
The Button is constrained Top: 0 (to topLabel), Leading: 8, Trailing: 8
The Bottom Label is constrained Top: 0 (to button), Leading: 8, Trailing: 8
The "shim view" is constrained Trailing: 8, Top: 0 (*to the top of bottom label*), Bottom: 4 (to the contentView)
The "shim view" is also given a Height constraint of 21, with Priority: 999 -- and that Height constraint is connected to an IBOutlet in the cell class.
The key is that we will adjust the shim's Height constraint's .constant to expand/collapse the cell.
On init, we set the .constant to 0 - this will leave the Bottom Label at its content-determined height, but won't be visible because it will be clipped by the cell's contentView.
When we want to "reveal/conceal" the label, we'll animate the height .constant of the shim.
Result:
And, the result after clearing the background colors:
Here is the code:
//
// ExpandCollapseTableViewController.swift
//
// Created by Don Mag on 6/19/18.
//
import UIKit
class ExpandCollapseCell: UITableViewCell {
#IBOutlet var topLabel: UILabel!
#IBOutlet var theButton: UIButton!
#IBOutlet var bottomLabel: UILabel!
#IBOutlet var theShim: UIView!
#IBOutlet var shimHeightConstraint: NSLayoutConstraint!
var myCallBack: (() -> ())?
#IBAction func didTap(_ sender: Any) {
myCallBack?()
}
}
class ExpandCollapseTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ExpandCollapseCell", for: indexPath) as! ExpandCollapseCell
cell.topLabel.text = "Index Path - \(indexPath)"
cell.bottomLabel.text = "Line 1\nLine 2\nLine 3\nLine 4"
// init to "collapsed"
// in actual use, this would be tracked so the row would remain expanded or collapsed
// on reuse (when the table is scrolled)
cell.shimHeightConstraint.constant = 0
if true {
cell.topLabel.backgroundColor = .clear
cell.theButton.backgroundColor = .clear
cell.bottomLabel.backgroundColor = .clear
cell.theShim.backgroundColor = .clear
}
cell.myCallBack = {
UIView.animate(withDuration: 0.3) { // It must be 0.3
self.tableView.beginUpdates()
cell.shimHeightConstraint.constant = (cell.shimHeightConstraint.constant == 0) ? cell.bottomLabel.frame.size.height : 0
self.tableView.layoutIfNeeded()
self.tableView.endUpdates()
}
}
return cell
}
}
I have had to do a similar animation several times before. The way to solve this is to define the height of your stack view and independently your cell's content view. Then when you want the cell's height to change, you only update the content view's height constraint.
A good way to determine a views height is to use the intrinsicContentSize property. Override this if you need a different value from the inherited one.
Another way to be notified of a views size change is to create a subclass with a delegate or a closure which is called from the subclassed views frame property and passes the new size to whoever is listening for it.
In my tableViewController i use a custom cell which contains an imageView, two labels and some other irrelevant elements. Also there's a constraint, of which the constant value shall be changed, if a certain condition is given. This works fine on the first sight but if i scroll down and get cells where the constraint constant is set to another value the some of the following cells kinda keep this constant from this previous cell and don't set it up at appearing.
This is the relevant part of my cellForRowAtIndexpath:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = whichCellToShow ? "ThisCell" : "TheOtherCellCell"
//...
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
if let customCell = cell as? CustomCellProtocol,
indexPath.section < data.count,
indexPath.row < data[indexPath.section].count {
customCell.display(data: data[indexPath.section][indexPath.row])
}
return cell
}
These are the relevant parts of my custom cell class:
class CustomCell: UITableViewCell, CustomCellProtocol {
#IBOutlet weak var picture: UIImageView!
#IBOutlet weak var title: UILabel!
#IBOutlet weak var weblink: UILabel!
#IBOutlet weak var titleIndentationConstraint: NSLayoutConstraint!
//...
func display(data: Data) {
//...
title.text = data.title
//...
weblink.text = data.hasWeblink ? URLUtilities.hostOfURL(urlstr: data.sourceUrl) : nil
weblink.isHidden = !data.hasWeblink
//This is the spot where things seem to go wrong
titleIndentationConstraint.constant = data.hasWeblink ? weblink.frame.height : 0
setNeedsLayout()
layoutIfNeeded()
}
}
If data.hasWeblink == true the constraint constant shall be equal to the height of the weblink label, if it's false it shall be 0.
My first thought was, that the view recycling of UITableView could collide with titleIndentationConstraint.constant = data.hasWeblink ? weblink.frame.height : 0. That's why i called setNeedsLayout()and layoutIfNeeded()straight afterwards, but this doesn't seem to help. All the other elements of the cell do their job correctly.
Also i'm setting tableView.rowHeightand tableView.estimatedRowHeightinside the viewDidLoad() of my tableViewController.
Does anybody have an idea what in particular is going wrong there or what i forgot to do? Thanks in forward!
Fixed it myself, had to change some constraint priorities of the title label:
My title label has a bottom constraint which is always greater or equal than 8 pts. Also i limited the number of lines to 4 and told the label to truncate the text's tail, if it's too long to be displayed. Funnily always, if the text was truncated, i got this silly behavior described above, so it has absolutely nothing to do with my titleIndetantionConstraint. Seems like if the text has to be truncated the label aligned itself to the bottom constraint and kinda dragged the content to center left (so watch what content mode says in the property editor, it should be left by default). While changing the label's content mode seems like a bad idea to me, i lowered the priority of the title label bottom constraint by 1 so it aligns itself to the top of the picture, like i planned. Hopefully this helps people how have similar issues B).
I have an UITableView which consists of prototype cells. I want to put an UIButton inside the bottom of the UITableView using Interface Builder.
I added the UIButton in the footer of the UITableView:
I added a purple background for the Footer View and a green background colour for the UITableView. In the picture above it shows the Button at the bottom of the footer. However this isn't equal to the bottom of the UITableView.
The GIF below displays that the button is placed bellow the cells but not inside the bottom of the UITableView. I want it to appear at the bottom in the UITableView. Not under the UITableView. The following GIF displays this problem:
My question is: How do I set an UIButton inside an UITableView at the bottom of the UITableView using Interface Builder?
This is what I want to achieve (From Apple's ResearchKit):
Edit: The UIButton should be inside the UITableView. Suggestions where the UIButton is placed outside the TableView and pinned underneath don't achieve my goal.
You are setting footer width wrong.Set it fixed height so that button sticks to that particular height(Should be Fixed like 60px)
Check Demo Code for Storyboard structure and constraints
So I had to slightly swizzle it, but got it working by doing the below things:
Pull the UIButton out to the same level in the view heirarcy as
the tableview.
Embed the tableview and the button inside a view
Embed the above view inside another view
Pin edges of view #3 (Pinned View) to superview
Pin top, left & right edges of view #2 (Resizing View) to view #3 edges. And set a constraint of equal height to view #3.
Set an outlet in the view controller for the equal height constraint
The view heirarcy in IB should look like this:
Now in the view controller code, you need to do the following things:
Create instance var for the keyboard offset value
var keyboardOffset: CGFloat = 0
set notifications and observers for the keyboard willShow and
willHide
notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(_:)), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(_:)), name:NSNotification.Name.UIKeyboardWillHide, object: nil)
In keyboardWillShow, cache the keyboard height value.
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
keyboardOffset = keyboardSize.height
}
Create didSet method on the keyboardOffset var, and animate the height of the view by that value each time it is set
var keyboardOffset: CGFloat = 0 {
didSet {
resizingViewHeight.constant = -keyboardOffset
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
}
}
Make sure you set the offset back to 0 in keyboardWillHide
keyboardOffset = 0
Every time the keyboard now appears, the view that is containing the tableview will reduce in size and therefore pull the contents up with it, providing the shrinking tableview effect that you are hoepfully looking for!
Add a view that contains the UIButton to the bottom of the UIViewController where the UITableView is. Give it the constraints to attach to left, right and bottom side of super view and probably a fixed height.
Then attach the UITableView's bottom constraint to the top of the view that contains the UIButton.
You should get the effect you're looking for.
NOTE: For the button you can give centered Y and X in superview constraints to keep it centered.
Footer is apperead always after the last cell of your table view so your output is correct.
If you wanted the button bottom of tableview then add button below the tableview in hierarchy not as a footer. But it makes your button static that means it didn't matter how much cells you have, button is always button of the tableView but it is not a scrollable like as it is now.
I tried the accepted answer, but couldn't get it to work. I found that the footer view always stayed pinned to the bottom of the screen, regardless of the size of the TableView (just as if it were a sibling of the TableView). I ended up following an approach suggested here: https://stackoverflow.com/a/18047772/5778751 The basic idea is that you programmatically determine the height of the TableView and depending on the result, you EITHER display a footer internal to the TableView OR display a view which is a sibling of the TableView.
I have a perfect solution for this problem. Using default was never that meaningful in my life.
The button under the view is also a table view cell from another section but its configuration of header height and interior design is just different from the above cells.
So I have five different sections. The first three of them are standard table view cells(SettingTableViewCell) but the last two(cache and version) are custom buttons. In the header title, I init for those empty titles.
enum Section: Int {
case adjustSettings
case about
case agreements
case cache
case version
static var numberOfSections: Int { return 5 }
var reuseIdentifier: String { return "SettingTableCell" }
var headerTitle: String? {
switch self {
case .adjustSettings: return "settings.adjust.section.title".localized
case .about: return "settings.headertitle.about".localized
case .agreements: return "agreement.title".localized
case .cache: return ""
case .version: return ""
}
}
Then I configured with cell will be in which section with below code. Cache and version have only one cell which will be our buttons.
var cells: [CellType] {
switch self {
case .adjustSettings:return [.notification,.language ]
case .about: return [.rate, .contact, .invite]
case .agreements: return [.membership, .kvkk, .illuminate]
case .cache: return [.cache]
case .version: return [.version]
}
}
I have three different set functions inside my settingsTableViewCell.
For setting up standard table view cell -> .setDefault(text: text)
For setting up my clean cache button -> .setCache(text: text)
Last for shoving version info -> .setVersion(version: version)
with the above cellForRowAt, I am switching rows and setting them up accordingly. My default is .setDefault
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else {
assertionFailure()
return UITableViewCell()
}
let row = section.cells[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: section.reuseIdentifier) as! SettingTableCell
switch row {
case .version:
cell.setVersion(version: getVersion())
case .cache:
ImageCache.default.calculateDiskCacheSize(completion: { size in
if size == 0 {
cell.setCache(text: "settings.clear.data".localized)
} else {
let byte = Int64(size)
let fileSizeWithUnit = ByteCountFormatter.string(fromByteCount: byte, countStyle: .file)
cell.setCache(text: "settings.cler.data.with.string".localized + "(\(String(describing: fileSizeWithUnit)))")
}
})
default:
cell.setDefault(text: row.text)
}
return cell
}
You can adjust button heights as below by switching section.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let section = Section(rawValue: indexPath.section) else { return 0 }
switch section {
case .cache: return 44
case .version: return 44
default: return 56.0
}
You can adjust the gap between each button as below.
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard let section = Section(rawValue: section) else { return 0 }
switch section {
case .adjustSettings: return 46
case .about: return 46
case .agreements: return 46
case .cache: return 9
case .version: return 0.5
default: return 46
}
And finally, this is my cell where I set .set functions to customize each cell as I pleased.
class SettingTableCell: UITableViewCell {
#IBOutlet weak var line: UIView!
#IBOutlet weak var content: UIView!
#IBOutlet weak var arrowView: UIView!
#IBOutlet weak var labelSetting: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
func setVersion(version: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = .clear
labelSetting.label(textStr: version, textColor: KSColor.neutral400.getColor(), textFont: .sfProTextRegular(size: 13), fontSize: 13, lineSpacing: -0.13, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setCache(text: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = KSColor.neutral100.getColor()
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: .sfProTextMedium(size: 14), fontSize: 14, lineSpacing: -0.14, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setDefault(text: String) {
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: UIFont.sfProTextMedium(size: 16), fontSize: 16, lineSpacing: -0.16, paragraphStyle: NSMutableParagraphStyle())
}
}
And the outcome is I have 5 sections but the last two are buttons.
A subclass of UITableViewCell contains a UIButton with multi-line text, i.e. property numberOfLines = 0.
The table view cells vary in height, so the cell height is set to UITableViewAutomaticDimension.
The cell height adapts when adding a UILabel with multiple text lines. However it does not adapt with a UIButton, in fact also the frame of the button does not adapt to the frame of its titleLabel.
What can I do to make the table view cell and its content view adapt to the button height?
class MyButtonCell: UITableViewCell {
var button: UIButton!
var buttonText: String?
convenience init(buttonText: String?) {
self.init()
self.buttonText = buttonText
button = UIButton(type: UIButtonType.System)
button.titleLabel?.numberOfLines = 0
button.titleLabel?.lineBreakMode = .ByWordWrapping
button.contentHorizontalAlignment = .Center
button.titleLabel?.textAlignment = .Center
button.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubView(button)
NSLayoutConstraint.activateConstraints([
button.topAnchor.constraintEqualToAnchor(self.contentView.topAnchor),
button.bottomAnchor.constraintEqualToAnchor(self.contentView.bottomAnchor),
button.rightAnchor.constraintEqualToAnchor(self.contentView.rightAnchor),
button.leftAnchor.constraintEqualToAnchor(self.contentView.leftAnchor)
])
button.setTitle(buttonText, forState: .Normal)
button.setTitleColor(buttonTextColor, forState: UIControlState.Normal)
button.titleLabel?.font = buttonFont
}
}
The cell height is calculated automatically with:
tableView.estimatedRowHeight = 100
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
UPDATE:
Example project on https://github.com/mtrezza/ButtonCellHeightBug
Filed Apple Bug Report #26170971.
The bug results in this:
Fully dynamic height for table view cell is achievable by 1) using estimated row height, 2) setting rowHeight to AutoDimension, 3) and most importantly using constraints in your xib/storyboard. The cell can contain buttons/labels or whatever UI components you'd like to have, as long as you constrain them properly, particularly to make sure things are constrained vertically so table view can figure out the cell height. And in this way you don't have to calculate height for dynamic text, no need for sizeToFit/sizeThatFit, and it works for different screen sizes.
You should use estimatedHeightForRowAtIndexPath. On your button, you can call sizeToFit(), which will resize it to contain the text.
Also, if you set the estimated size on the tableView (as you did), you usually don't need to call the heightForRowAtIndexPath, or estimatedHeightForRowAtIndexPath, and the tableView will set it for you.
EDIT:
I created a test project, and you seem to be correct. Using a UIButton setTitle does not resize the cell.
A workaround, is to do the calculation using a label in heightForRowAtIndexPath, and return that value + any padding. Then in cellForRowAtIndexPath, you can still set the title on the button and it will appear.
//paragraphs is just a string array.
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let label = UILabel(frame: CGRectMake(0,0,tableView.frame.width, <your prototype height>))
label.numberOfLines = 0
label.text = paragraphs[indexPath.row]
label.sizeToFit()
print(label.frame.height)
return label.frame.height
}
Bug in iOS?
The problem is that the internal UIButtonLabel resizes correctly, but the actual UIButton does not.
I've worked around this by extending UIButton and overriding a couple of things:
override func layoutSubviews() {
super.layoutSubviews()
self.titleLabel?.preferredMaxLayoutWidth = self.titleLabel?.frame.size.width ?? 0
}
override var intrinsicContentSize: CGSize {
return self.titleLabel?.intrinsicContentSize ?? CGSize.zero
}
You'll also need to make sure that titleLabel?.numberOfLines = 0 and titleLabel?.lineBreakMode = .byWordWrapping.