Is it possible to self-size a UITableViewCell in iOS 8 without creating a Custom UITableViewCell?
I thought that the standard UITableViewCell types (UITableViewCellStyleDefault, UITableViewCellStyleSubtitle, UITableViewCellStyleValue1, UITableViewCellStyleValue2) had built in auto layout constraints. This is confirmed by the fact that the constraints for non-custom cells cannot be changed in Storyboard.
But when I use a non-custom cell of type UITableViewCellStyleValue1, set it up in Storyboard, set numberOfRows for textLabel and detailTextLabel to 0, and set the viewDidLoad code as below, only the textLabel of the cell is accounted for in the autosizing of the height of the cell. If the detailTextLabel ends up displaying on more lines than the textLabel, the text for detailTextLabel spills out over the top and bottom edges of the cell. Again, the cell does resize properly for the textLabel, but seems to ignore the detailTextLabel in its resizing process.
The main thing I need to know is - do I need to create a custom cell even for the rows that can use a standard cell if I want to properly support Dynamic Text and Self-Sizing?
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView setEstimatedRowHeight:DEFAULT_ROW_HEIGHT];
[self.tableView setRowHeight:UITableViewAutomaticDimension];
}
I just tried this in iOS 10/XCode 8 (same results in iOS 9/XCode 7) with the different cell types and it looks like it's possible ONLY for the textLabel and not for the detailTextLabel.
(basically repeating the issue that the OP mentioned)
ViewController code that sets some text alternately on detailTextLabel and textLabel.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if indexPath.row % 2 == 0 {
cell.textLabel?.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
cell.detailTextLabel?.text = "<- textLabel"
} else {
cell.textLabel?.text = "detailTextLabel ->"
cell.detailTextLabel?.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
}
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
}
Make sure you set the textLabel and textDetailLabel's line property to 0 and here are the results.
Basic Cell
Right Detail Cell
Left Detail Cell
Subtitle Cell
I'll report this as a bug.
Related
I've got 2 Label in cell with dynamic content coming from backend and trying to make dynamic changing height of cell in TableView. How to correctly do this?
This Setting custom UITableViewCells height doesn't help me
You can do this by 2 ways.
You can do this by setting below lines of code.
tblVW.estimatedRowHeight = 64.0
tblVW.rowHeight = UITableView.automaticDimension
by using Delegates
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 64.0
}
Note: UILabel numberOfLines shoud be 0 for auto height and there's no any fixed height set to your cell UILabel.
You can use this two function:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 64.0
}
Remember to set the constraints for your two label inside the cell or the automaticDimension won't work
First, you got to make sure you've set the constraints of the cell's subviews properly, for your case here how they should look like:
(Something you'll probably miss- the sub label's Content Hugging Priority has been decreased
)
After you've set your constraints properly, we can proceed to the next step- our UITableViewController (if you got a UIViewController with a UITableView inside him, your class will look different. I'm not covering this- that's for another topic)
You should change your ViewDidLoad function to look like this:
override func viewDidLoad() {
super.viewDidLoad()
// That what causes the tableView to dynamicaly set the cell's height- it will work properly only if your constraints are set properly
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.estimatedRowHeight = 80 // for normal conditions this type of cell would have height of 80- it's an estimated height after all...
}
So that's it- here's how the final result looks like when I've used my own dataset
Final code of UITableViewController:
class TableViewController: UITableViewController {
struct ListItem{
var header: String
var sub: String
}
var ourDataHolder: [ListItem] = []
func setupOurDataModel(){
// Here we are setting different ListItems to our data holder, each ListItem should have a different height when displayed
let shortListItem = ListItem(
header: "Header",
sub: "Sub"
)
let mediumListItem = ListItem(
header: "eu mattis diam imperdiet",
sub: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
)
let longListItem = ListItem(
header: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas porttitor maximus purus quis varius. Vivamus non ornare elit. Integer nec lobortis urna. Praesent nec lorem quis libero condimentum commodo. Vestibulum elementum lacinia purus ac imperdiet. Nulla iaculis velit quis leo condimentum, eu mattis diam imperdiet. Integer quis ligula metus.",
sub: "In vulputate magna sit amet mi faucibus luctus. Nullam finibus viverra fermentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Proin ac arcu orci. Nullam lobortis augue ut purus ornare dapibus. Nulla id vehicula orci. Fusce pulvinar massa ut erat eleifend venenatis. Phasellus erat nulla, placerat a tincidunt nec, varius eu tellus. Cras quis augue non nulla elementum malesuada ut ac purus. Maecenas neque sem, tristique sit amet laoreet vitae, cursus porttitor tortor. Pellentesque tincidunt ligula vel est tempus finibus. Maecenas ac sem ac massa auctor posuere non ut tellus. Proin cursus nibh a aliquam tincidunt."
)
ourDataHolder = [shortListItem, mediumListItem, longListItem]
}
override func viewDidLoad() {
super.viewDidLoad()
setupOurDataModel()
// That what causes the tableView to dynamicaly set the cell's height- it will work properly only if your constraints are set properly
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.estimatedRowHeight = 80 // for normal conditions this type of cell would have height of 80- it's an estimated height after all...
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int { return 1 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return ourDataHolder.count }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// be sure to set the cell's reusableIdentifier on storyboard- I've set my to "customCell"
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! CustomTableViewCell
cell.headerLabel.text = ourDataHolder[indexPath.row].header
cell.subLabel.text = ourDataHolder[indexPath.row].sub
// Don't forget to set 0 for the number of lines in your label- that can be set using storyboard or programmaticaly.
// 0 means there won't be any limitation on the maximum number of lines
cell.headerLabel.numberOfLines = 0
cell.subLabel.numberOfLines = 0
return cell
}
}
I'm not adding the CustomTableViewCell code- that's for another topic
Scenario
I am using the automaticDimension option on UITableView. I'd like to have a single UILabel in my cell that is self-sizing to fit the text.
Setup
Here you can see how I setup the label. The edges are equal to the contentView margins.
Problem
The height is set to 37.0 points on the phone when the text fits one line. 44.0 should be the minimum.
Question
How do I have to setup the layout to maintain a minimum cell height of 44.0 (default height, fitting the other cells)?
Edit:
Using the built-in 'basic' TableViewCell with numberOfLines = 0 seems to be the most easy and best solution! Suggested by eddwinpaz. Thank you all.
Change the top anchor from constraint(equalTo:) to greaterThanOrEqualTo: and the bottom one to lessThanOrEqualTo:.
Then make the label centered on the Y-Axis.
Then you should set cell. heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
You need to use numberOfLine = 0 in case you are just working with a Single UILabel. Else. You need to use constrains.
import UIKit
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuseIdentifier")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
cell.textLabel?.numberOfLines = 0
cell.textLabel?.text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
return cell
}
}
In case you need to use a custom UILabel like your case.
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomCell.self, forCellReuseIdentifier: "reuseIdentifier")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) as! CustomCell
cell.myLabel.text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
return cell
}
}
class CustomCel: UITableViewCell {
let myLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
func setupViews(){
addSubview(myLabel)
myLabel.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
myLabel.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
myLabel.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
myLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I have a UICollectionView with a header of type UICollectionReusableView.
In it, I have a label whose length varies by user input.
I'm looking for a way to have the header dynamically resize depending on the height of the label, as well as other subviews in the header.
This is my storyboard:
This the result when I run the app:
Here's an elegant, up to date solution.
As stated by others, first make sure that all you have constraints running from the very top of your header view to the top of the first subview, from the bottom of the first subview to the top of the second subview, etc, and from the bottom of the last subview to the bottom of your header view. Only then auto layout can know how to resize your view.
The following code snipped returns the calculated size of your header view.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
// Get the view for the first header
let indexPath = IndexPath(row: 0, section: section)
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
// Use this view to calculate the optimal size based on the collection view's width
return headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required, // Width is fixed
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
}
Edit
As #Caio noticed in the comments, this solution will cause a crash on iOS 10 and older.
In my project, I've "solved" this by wrapping the code above in if #available(iOS 11.0, *) { ... } and providing a fixed size in the else clause. That's not ideal, but acceptable in my case.
This drove me absolutely crazy for about half a day.
Here's what finally worked.
Make sure the labels in your header are set to be dynamically sizing, one line and wrapping
Embed your labels in a view. This will help with autosizing.
Make sure the constraints on your labels are finite. ex: A greater-than constraint from the bottom label to the reusable view will not work. See image above.
Add an outlet to your subclass for the view you embedded your labels in
class CustomHeader: UICollectionReusableView {
#IBOutlet weak var contentView: UIView!
}
Invalidate the initial layout
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
collectionView.collectionViewLayout.invalidateLayout()
}
Lay out the header to get the right size
extension YourViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
if let headerView = collectionView.visibleSupplementaryViews(ofKind: UICollectionElementKindSectionHeader).first as? CustomHeader {
// Layout to get the right dimensions
headerView.layoutIfNeeded()
// Automagically get the right height
let height = headerView.contentView.systemLayoutSizeFitting(UILayoutFittingExpandedSize).height
// return the correct size
return CGSize(width: collectionView.frame.width, height: height)
}
// You need this because this delegate method will run at least
// once before the header is available for sizing.
// Returning zero will stop the delegate from trying to get a supplementary view
return CGSize(width: 1, height: 1)
}
}
Swift 3
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize{
return CGSize(width: self.myCollectionView.bounds.width, height: self.mylabel.bounds.height)
}
https://developer.apple.com/reference/uikit/uicollectionviewdelegateflowlayout/1617702-collectionview
You could achieve it by implementing the following:
The ViewController:
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
// the text that you want to add it to the headerView's label:
fileprivate let myText = "Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda."
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind,
withReuseIdentifier: "customHeader",
for: indexPath) as! CustomCollectionReusableView
headerView.lblTitle.text = myText
headerView.backgroundColor = UIColor.lightGray
return headerView
default:
fatalError("This should never happen!!")
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "customCell", for: indexPath)
cell.backgroundColor = UIColor.brown
// ...
return cell
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
// ofSize should be the same size of the headerView's label size:
return CGSize(width: collectionView.frame.size.width, height: myText.heightWithConstrainedWidth(font: UIFont.systemFont(ofSize: 17)))
}
}
extension String {
func heightWithConstrainedWidth(font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: UIScreen.main.bounds.width, height: CGFloat.greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox.height
}
}
The custom UICollectionReusableView:
class CustomCollectionReusableView: UICollectionReusableView {
#IBOutlet weak var lblTitle: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// setup "lblTitle":
lblTitle.numberOfLines = 0
lblTitle.lineBreakMode = .byWordWrapping
lblTitle.sizeToFit()
}
}
#Pim's answer worked for me but as #linus_hologram in the comment section mentioned this solution makes AutoLayout complain about unsatisfiable constraints. I found a simple workaround:
In collectionView(_:layout:referenceSizeForHeaderInSection:) instead of getting a reference to the view that your instance of UICollectionView wants to reuse using collectionView(_:viewForSupplementaryElementOfKind:at:) just create an instance of your header view class on the fly and add a width constraint so that AutoLayout will be able to calculate your header's width:
let headerView = YourCustomHeaderClass()
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.widthAnchor.constraint(equalToConstant: collectionView.frame.width).isActive = true
return headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
I have a cell that contains a few stackviews, bottom stackView contains a textView and a custom separator.
I want to create an option, when user tap on cell, it shows whole text of tapped text view, so number of maximum lines in that cell is 0 and in other cells should be 3.
I used this tutorial http://www.roostersoftstudios.com/2011/04/14/iphone-uitableview-with-animated-expanding-cells/ and I modified it a little, my code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(iden_tableViewCell4) as! TableViewCell4
if let selectedCellIP = selectedIndexPath {
if indexPath == selectedCellIP {
cell.textTextVIew.textContainer.maximumNumberOfLines = 0
}
else {
cell.textTextVIew.textContainer.maximumNumberOfLines = textVIewMaxNumberOfLines
}
}
else {
cell.textTextVIew.textContainer.maximumNumberOfLines = textVIewMaxNumberOfLines
}
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
//The user is selecting the cell which is currently expanded
//we want to minimize it back
if selectedIndexPath == indexPath {
selectedIndexPath = nil
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
return
}
//First we check if a cell is already expanded.
//If it is we want to minimize make sure it is reloaded to minimize it back
if let selIndexPath = selectedIndexPath {
let previousPath = selIndexPath
selectedIndexPath = indexPath
tableView.reloadRowsAtIndexPaths([previousPath], withRowAnimation: .Fade)
}
//Finally set the selected index to the new selection and reload it to expand
selectedIndexPath = indexPath
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
in my viewDidLoad I set
tableView.estimatedRowHeight = 160
tableView.rowHeight = UITableViewAutomaticDimension
Expansion and contraction work well, but textView height of other cells has a strange behavior. When first cell is extended, an I scroll down, the last is extended too, and should not be.
Dynamically Resize UITableViewCell upon Selection
Playing with individual selection indexes is dangerous business. Not only are you likely to miss corner case conditions in didSelectRowAtIndexPath, it cannot possibly work for multiple selections.
You should split the cell expansion/compression notification into 2 distinct blocks (no need to keep track of selectedIndexPath, more robust code):
Expand
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
self.reloadRowsAtIndexPaths(tableView,
indexPath:indexPath)
}
Contract
override func tableView(_ tableView: UITableView,
didDeselectRowAt indexPath: IndexPath) {
self.reloadRowsAtIndexPaths(tableView,
indexPath:indexPath)
}
Selection is destroyed by selectRowAtIndexPath
This prevents didDeselectRowAtIndexPath from ever being invoked.
A workaround is to cache the entire selection, not individual indexes, and to restore such selection after reload.
Complete code:
Notice when and how cacheSelectedRows is maintained. This uses a plain UITableViewCell. All it needs is a reuseIdentifier.
class TableViewController: UITableViewController {
var cacheSelectedRows:[IndexPath]? = nil
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 160
tableView.rowHeight = UITableViewAutomaticDimension
}
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: "reuseIdentifier", for: indexPath)
if let textLabel = cell.textLabel {
textLabel.backgroundColor = UIColor.clear
textLabel.numberOfLines = 1
if let cacheSelectedRows = cacheSelectedRows {
textLabel.numberOfLines = (cacheSelectedRows.contains(indexPath)) ? 0 : 1
}
textLabel.text = "\(1 + indexPath.row), Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.reloadRowsAtIndexPaths(tableView, indexPath:indexPath)
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
self.reloadRowsAtIndexPaths(tableView, indexPath:indexPath)
}
func reloadRowsAtIndexPaths(_ tableView: UITableView, indexPath: IndexPath) {
cacheSelectedRows = tableView.indexPathsForSelectedRows
tableView.reloadRows(at: [indexPath], with: .fade)
// Restore selection
if let cacheSelectedRows = cacheSelectedRows {
for path in cacheSelectedRows {
self.tableView.selectRow(at: path, animated: false, scrollPosition: .none)
}
}
}
}
Demo
► Find this solution on GitHub and additional details on Swift Recipes.
I didn't find answer, why the strange behavior of textView and cell height is happening, but I have solution for my problem.
So it looks like tableView.estimatedRowHeight = 160 and tableView.rowHeight = UITableViewAutomaticDimension don't respect textView's maximum number of lines for all cells. cellForRow works fine, it sets the limitation, but the rows height is sometimes expanded and sometimes contracted.
So I unwrapped textView and the bottom separator from StackView, replaced TextView with Label and set autolayaut constraints. Now it works fine, without strange behavior.
Currently I am messing around with swift and dynamic table cell heights. I developed a simple app for iOS8.1 on xcode6.1: https://github.com/ArtworkAD/DynamicCellTest
So to achieve a cell height that stretches with the cell's content I do the following:
in storyboard set label lines to 0
set labels font to system
set constraints for label in cell
add self.tableView.rowHeight = UITableViewAutomaticDimension
don't override heightForRowAtIndex method
Minimal code is needed:
class MyTableViewController: UITableViewController {
var entries:Array<String> = [String]()
override func viewDidLoad() {
super.viewDidLoad()
//create dummy content
var i = 0
while i < 10 {
entries.append("\(i) Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor")
entries.append("\(i+1) Lorem ipsum dolor sit amet")
i = i + 2;
}
self.tableView.rowHeight = UITableViewAutomaticDimension
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.entries.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("basic_cell", forIndexPath: indexPath) as UITableViewCell
var label = cell.viewWithTag(13)
if let unwrappedLabel = label as? UILabel {
unwrappedLabel.text = self.entries[indexPath.row]
}
return cell
}
}
The left image shows the result of the above code. The cell height grows with the content of the label, all nice. However when you click on the disclosure indicator to the detail view and move back again, you get the right image. Why is this happening??
A bad solution for this problem is to override this methods:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.tableView.reloadData()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.tableView.reloadData()
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
self.tableView.reloadData()
}
This way the above problem is solved, but this solution seems not right? A side effect of it is, that when self.tableView.reloadData() is called the table view port jumps to the first cell which doesn't look nice.
Does anyone has an idea what I am doing wrong? Feel free to clone my repo https://github.com/ArtworkAD/DynamicCellTest and test it out.
Adding this seems that it is able to fix rotation problem.
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
self.tableView.estimatedRowHeight = 36.5
}
However, I have another case that there are 4 labels in the cell, which has the same rotation problem, adding this is not enough and I ended up replacing the self.tableView.estimatedRowHeight = 36.5 with reloading visible cells.
I've just solved exactly that problem by overriding tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) in UITableViewDelegate object.
func tableView(tableView: UITableView,
estimatedHeightForRowAtIndexPath indexPath: NSIndexPath)
-> CGFloat {
return 200
}
Changing the exact returned value apparently have no effect. The cells are always properly self-sizing. I've got no idea why providing estimated height causes the autolayout to kick in, but it does. iOS 8.1.2, Xcode 6.1.
The link provides a good explanation of what needs to be done.
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
For your specific problem you are missing two lines of code.
In viewDidLoad under your tableView.rowHeight add self.tableView.estimatedRowHeight = 64
The above will provides allow the tableView to assign estimated height values for offscreen cells to enhance performance and it also calculates the scroll bar height.
The main problem is you are missing unwrappedLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds) in cellForRowAtIndexPath this tells the label its preferred maximum width so it can calculate the height it requires for multi-line text. Usually you would do this in the table view cell subclass.
I have tested this in your project and it works
You have to set estimatedRowHeight:
// setup automatic row height
self.tableView.rowHeight = UITableViewAutomaticDimension
// whatever you think is the approximate row height
self.tableView.estimatedRowHeight = 44
Also came across an alternative here http://useyourloaf.com/blog/2014/08/07/self-sizing-table-view-cells.html. A comment mentions -
Simply make the cell layout its subviews before returning it from cellForRowAtIndexPath:
[cell setNeedsDisplay];
[cell layoutIfNeeded];