I have a UITableView with custom cells that also contain UITableViews. I'm having problems getting the height for these cells.
I'm getting the height in heightForRowAtIndexPath and to do this I populate the child tableView (within the cell) and call layoutIfNeeded. This gives me the correct contentSize for this tableView. The problem is that the overall size for the cell is wrong and doesn't change. So calling cell.bounds.height returns an incorrect value.
let cell:GroupCell = tableView.dequeueReusableCellWithIdentifier("GroupCell") as GroupCell
cell.configure(field as SingleField, delegate: self) // populates data
cell.tableView.layoutIfNeeded()
// cell.tableView.contentSize is correct
return cell.bounds.height // is wrong - bounds haven't changed
Can anyone point me in the right direction ? BTW - I'm targeting iOS8 only.
Try to create subclass of UITableViewCell, add TableView delegates to it, use it like usual.
I think the problem is in UITableView. You've sad table have correct content size, but it have incorrect size. After calculating the correct content size you should change tableView height manually. I think it will help you. Tip: you can observe contentSize property and every time it will be changed you can resize table view. To do this - add height constrain to your table with low priority (750 for example) and than just change it when needed. In this case your table will have both content size and self.frame.heigh equal.
I managed to resolve this as follows:
I put a height constraint (Greater than or equal to) on the UITableView (within the Cell). I set the priority of this to High
I set the priority of the bottom margin to Low
When configuring my custom cell I call setNeedsUpdateConstraints(), which invalidates the height constraint by removing it, and then recalculates it from the content size of the table view before adding it back.
Finally, in heightForRowAtIndexPath cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height now returns the correct height
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let field = fields[indexPath.row]
let cell:GroupCell = tableView.dequeueReusableCellWithIdentifier("GroupCell") as GroupCell
cell.configure(field as SingleField, delegate: self)
return cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
}
func configure(field:SingleField,delegate:GroupDelegate){
self.field = field
self.delegate = delegate
nameLabel.text = field.name
tableView.reloadData()
setNeedsUpdateConstraints()
}
override func updateConstraints() {
tableView.removeConstraint(tableViewHeightConstraint)
tableViewHeightConstraint = NSLayoutConstraint(item: tableView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: tableView.contentSize.height)
tableView.addConstraint(tableViewHeightConstraint)
super.updateConstraints()
}
Related
I'm having trouble changing the relation on a constraint from storyboard. This is how the table view looks like at the moment.
When the more button is click, I want the table view to look like this.
Okay, so I drag the bottom constraint from the imageView into my tableViewCell.swift file and named it imageBottomConstraint. I tried imageBottomConstraint.relation = .greaterThan but gives me an error saying that it's only a get, so the relation is equalTo and I'm trying to change it to greaterThan.
Here is were I'm confused and stuck, so I'm trying to create a new constraint but don't really understand NSLayoutConstraint(item:, attribute:, relatedBy:, toItem:, attribute:, multiplier:, constant:). Don't understand item, attribute, relatedBy, toItem & attribute, can someone please help clarify.
My disaster code I thought would work but nope crashes instead.
#IBAction func showMoreBtn() {
let item = imageBottomConstraint.firstItem
let attribute1 = imageBottomConstraint.firstAttribute
let relatedBy = NSLayoutRelation.greaterThanOrEqual
let item2 = imageBottomConstraint.secondItem
let attribute2 = imageBottomConstraint.secondAttribute
let multiplier = imageBottomConstraint.multiplier
let constant = 12
let newConstraint = NSLayoutConstraint(item: item, attribute: attribute1, relatedBy: relatedBy, toItem: toItem, attribute: attribute2, multiplier: multiplier, constant: 12)
}
So as you can see I don't know what I am doing, when creating constraint programmatic. Would appreciate the help.
You can Manage the Expected Behaviour in heightForRowAt Cell Delegate of TableView
/// Use this function to Set Height of TableCells
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
/// Is index Path More Button is cliked ?
if indexPath.row == selectedIndex
{
/// If yes
return UITableViewAutomaticDimension
}
else
{
/// if No
/// Provide a Static Height That will Set your requiremment
return 160
}
}
selectedIndex its the index in which that more Button is Clicked , My requirement was to Expand one cell At a time, You can use a Array to manange the cells Need to be Expanded
You might not use the right solution. I would use the fact that cells can expand and adapt to there content by using UITableViewAutomaticDimension.
Here is a sample that could help you on your particular case:
https://www.appcoda.com/self-sizing-cells/
The idea would be:
fix with a constraint the height of your label
When you click the more button, you set that constraint to not active with yourConstraint.active = false and reload the cell by doing something like yourTableView.reloadRows
Apple doc say : reloadRowsAtIndexPaths:withRowAnimation:
:
Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.
My problem scenario is that i want to update a label in a cell on button click and also update the layout (i.e. a new view of the cell is being added or removed on a condition applied). if reloadRowsAtindexPath is called on the button click then the tableview randomly scroll down to some other row in the tableview. If only the cell content is updated on button click then the layout is not updated properly.
If anybody has faced the same issue with the reload?
Well this turned out trickier than I expected.
Here's how I implemented it. I am not really sure if this is a good way or not so take it with a grain of salt. Find link to the project below.
You need to know two things:
The default/normal cell height (which is basically the estimated height of cell).
Increase in height of cell after the view has been added to the cell (in my example I have taken the height to be 200).
When you tap on a button which is supposed to add the subview you call a completion handler passing the indexPath and heightToBeAdded(200 in this example) as parameters.
The completion will look something like this:
self.indexPath = iPath
self.cellHeight = self.defaultCellHeight + heightToBeAdded
UIView.beginAnimations("aaa", context: nil)
UIView.setAnimationDuration(1)
self.tableView.beginUpdates()
self.tableView.reloadRows(at: [iPath], with: .none)
self.tableView.endUpdates()
UIView.commitAnimations()
if heightToBeAdded > 0.0 {
self.addCellSubviews()
}
The iPath is the same indexPath that you sent a parameter. The cell height is calculated from the two things I have described above.
When endUpdates() calls the delegate and datasource methods would encounter the following method:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let iPath = self.indexPath else { return UITableViewAutomaticDimension }
if indexPath == iPath {
return cellHeight
}
else {
return UITableViewAutomaticDimension
}
}
This will increase the cell height. But what about the actual subview that needs to be added?
For that we have addCellSubviews() which is actually a completion block that is executed inside the UITableViewCell subclass. Call this only after end updates because by that time cell height will be calculated properly.
let yView = UIView()
yView.backgroundColor = .yellow
yView.translatesAutoresizingMaskIntoConstraints = false
self.addAsSubview(sView: yView)
NSLayoutConstraint.activate([
yView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 8),
yView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
yView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -275),
yView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8)
])
UIView.animate(withDuration: 1, animations: {
self.layoutIfNeeded()
})
Here is the result -
Link to the project
Please note there are some caveats(like tapping one cell closes the other) which I am sure you will be able to work out.
This was an issue with the estimated row height. changing the value to some other estimate fixed the issue of unwanted scrolling.
Mentioned in apple docs: "Additionally, try to make the estimated row height as accurate as possible. The system calculates items such as the scroll bar heights based on these estimates. The more accurate the estimates, the more seamless the user experience becomes."
I have a UITableView which has multiple UIStackView as items in it. In those UIStackView I add colored squares as subviews. The problem is, it heavily slows down the vertical scrolling.
How do I improve scrolling performance?
Here is how scrolling looks:
https://gfycat.com/DentalContentIndigowingedparrot
Here is the source code:
https://github.com/Cook10/StackTableProblem
You are missing the point of reusing cells. Your code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! MyTableViewCell
addStacksToTableCell(cell.stack1)
addStacksToTableCell(cell.stack2)
addStacksToTableCell(cell.stack3)
addStacksToTableCell(cell.stack4)
return cell
}
everytime you scroll down and a new cell is displayed, you are removing all views inside it (400 views) and your are creating new views (400 views), triggering the expensive layout and rerendering mechanism.
That happens every time your scroll.
This completely defeats the purpose of reusing cells. It's the same as creating a new cell every time.
Instead, create the views only once and when reusing the cells, only update the color of the internal views. Don't recreate views just to change their color.
Using some very naive changes:
Add to your cell:
var createdViews: Bool = false
var views1: [UIView] = []
var views2: [UIView] = []
var views3: [UIView] = []
var views4: [UIView] = []
Then replace your controller methods with:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
if !cell.createdViews {
cell.views1 = addStacksToTableCell(cell.stack1)
cell.views2 = addStacksToTableCell(cell.stack2)
cell.views3 = addStacksToTableCell(cell.stack3)
cell.views4 = addStacksToTableCell(cell.stack4)
cell.createdViews = true
} else {
// only update the views
cell.views1.forEach { $0.backgroundColor = UIColor.random() }
cell.views2.forEach { $0.backgroundColor = UIColor.random() }
cell.views3.forEach { $0.backgroundColor = UIColor.random() }
cell.views4.forEach { $0.backgroundColor = UIColor.random() }
}
return cell
}
func addStacksToTableCell(_ stack: UIStackView) -> [UIView] {
stack.removeAllArrangedSubviews()
var views: [UIView] = []
for i in 0..<ITEMS{
let image = UIImageView()
image.backgroundColor = UIColor.random()
stack.addArrangedSubview(image)
let width = NSLayoutConstraint(item: image, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 50)
let height = NSLayoutConstraint(item: image, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 50)
image.addConstraints([width,height])
views.append(image)
}
return views
}
It's not 100% connected to this particular case, but another way to improve cell scrolling is :
In iOS, you can use Auto Layout to define the height of a table view cell; however, the feature is not enabled by default.
Normally, a cell’s height is determined by the table view delegate’s tableView:heightForRowAtIndexPath: method. To enable self-sizing table view cells, you must set the table view’s rowHeight property to UITableViewAutomaticDimension. You must also assign a value to the estimatedRowHeight property. As soon as both of these properties are set, the system uses Auto Layout to calculate the row’s actual height.
tableView.estimatedRowHeight = 85.0
tableView.rowHeight = UITableViewAutomaticDimension
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.
Additionally, try to make the estimated row height as accurate as possible. The system calculates items such as the scroll bar heights based on these estimates. The more accurate the estimates, the more seamless the user experience becomes.
Wish you all the best.
I have attached a UItextView in tableview cell. Now I want to insert an UIimage behind it like a chat bubble.But everytime I scroll the tableview , the Uitextview becomes smaller in width and longer in height. The text also gets resized from single to multi line. Also ,the same thing happens when I scroll back to an earlier cell ( for example the first row).
I can't figure out why this is happening.
My code is:
(SampleTableViewCell is my custom UItableViewCell class and lblTitle is my UitextView outlet.)
func tableView(tableView: UITableView, willDisplayCell cell: SampleTableViewCell, forRowAtIndexPath indexPath: NSIndexPath)
{
cell.lblTitle.removeConstraints(cell.lblTitle.constraints)
let stringTitle = carName[indexPath.row] as String //carName is an array
cell.lblTitle.text=stringTitle
cell.lblTitle.sizeToFit()
let h = cell.lblTitle.frame.size.height
let w = cell.lblTitle.frame.size.width
let constraints = NSLayoutConstraint(item: cell.lblTitle, attribute: .Width, relatedBy: .Equal, toItem: cell.lblTitle, attribute: .Height, multiplier: 0, constant: w)
cell.lblTitle.addConstraint(constraints)
let kl = UIImage(imageLiteral: "bubble")
let imageView = UIImageView(frame:CGRectMake(0, 0, w, h))
imageView.image = kl
imageView.alpha = 0.4
cell.lblTitle.addSubview(imageView)
cell.lblTitle.sendSubviewToBack(imageView)
}
It is the problem of autolayout.
You are just giving height and width constraint to textView. what about x and y position?
And you haven't set constraints for your imageview!!
If you are giving constraint to one object then you required to give every object related to it.
So set proper constraints and your problem will be solved.
Instead of adding bubble imageview and constraints programmatically,using storyboard you can add imageview in the tableview cell first and then UITextView of same size as imageview and add constraints.Then you can add your logic at cellForRowAtIndexPath delegate for lblTitle's text and bubble image. Make sure to set lblTitle's background to ClearColor.
I have a label in a custom cell and it won't resize. Here is the code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myCell2 = tableView.dequeueReusableCellWithIdentifier("privatecell1", forIndexPath: indexPath) as! YourPrivateControllerCell
myCell2.barfront1.frame.size.width = 200
myCell2.barfront1.layer.masksToBounds = true
return myCell2
}
I know I can set the width by adding a constraint but the label will be a different size for every row. The code works for a normal view controll view but doesn't seem to work for a tableview cell/row. The actual code will be:
myCell2.barfront1.frame.size.width = myCell2.barback1.frame.size.width * percent
but I cant even get the label to resize to 200.
You can use constraints and connect the constraints with #IBOutlet like this. Of course you have to put this in the YourPrivateControllerCell.
#IBOutlet weak var labelWidth: NSLayoutConstraint? = nil
Then call the following method will be change the width of you label.
labelWidth.constant = 200
Good Luck.
I got it to work. Instead of setting a width for the label I added a layout width constraint. Code:
myCell2.barfront1.addConstraint(NSLayoutConstraint(item: myCell2.barfront1, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: myCell2.barback1.frame.size.width * barwidth1[indexPath.row] * 0.01))
Now the label is a different size for every row.