I'm trying to resize a label's height when my custom cell is selected (to allow it to expand so more text is visible).
I'm obviously missing something fundamental though because the frame is the exact same after I try to draw the new CGRect.
Here's the relevant code:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexPath = tableView.indexPathForSelectedRow()
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! BasicCell
UIView.animateWithDuration(0.3) {
tableView.beginUpdates()
currentCell.subtitleLabel!.frame = CGRectMake(currentCell.subtitleLabel!.frame.origin.x, currentCell.subtitleLabel!.frame.origin.y, currentCell.subtitleLabel!.frame.size.width, 100)
currentCell.subtitleLabel!.numberOfLines = 0
tableView.endUpdates()
}
}
Any help would be appreciated.
You should use
-reloadRowsAtIndexPaths:withRowAnimation:
inside beginUpdate and endUpdate pair. Simply setting the frame is not enough for table view to know which cell needs to be updated.
Also there's no need to put your update code inside animation block. It's animated inherently.
Related
I have a UITableView that I fill with autosizing cells.
UITableView setup is fairly simple:
tableView.estimatedRowHeight = 70
tableView.rowHeight = UITableViewAutomaticDimension
Exactly like Apple recommends here: https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSelf-SizingTableViewCells.html
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.
When configuring a cell I also disable/enable some constraints to achieve the needed look. That’s where things get interesting. Cell layout is not updated until the cell is reused. Literally. You can call layoutIfNeeded(), setNeedsLayout(), layoutSubviews() or any other method there is, there is no way you will force the cell to update its layout.
All other aspects work pretty good: labels do change their text, you hide/unhide the views, but layout is stuck until the cell is reused.
Question: what causes it and how to avoid this behavior?
I had your problem too. Instead of remove
tableView.estimatedRowHeight = 70
I just added a layoutIfNeeded at the end of the cellForRow method, just before return the cell itself:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as? MyCustomCell
...
cell?.layoutIfNeeded()
return cell!
}
Result: the cell layout is perfect always, the first time and every after reuse.
Unfortunately, none of the provided answers/comments worked out for me. I always ended up with an initially incorrect layout. Only after reusing the cell, or calling reloadData() (on the table view) it was displayed correctly.
The following was the only thing, that worked for me in the end. I'm not a big fan of such hacks, but after spending about half a day on this seemingly very simple layout issue, I just gave up and went with it. >.<
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Alternatively you could also call reloadData it in viewDidAppear (without the DispatchQueue hack), but then you can clearly see the "jump" when the layout jumps from "incorrect" to "correct".
Anyway, just wanted to share my experience and hope this helps someone else. Cheers!
In my case, the issue was caused by estimatedRowHeight.
Simply removing this line
tableView.estimatedRowHeight = 70
fixed my problems. Cell properly updated its layout and it almost fixed my issues.
But, most likely, you’re going to get another trouble, with your cell’s height being set to 43.5 points. You log will also be filled with auto layout errors, that will include a line like this
<NSLayoutConstraint:0x600000097570 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7fd4ee511d20.height == 43.5 (active)>
Apparently, if you won’t provide estimatedRowHeight, table view puts a 43.5 points height constraint on your cell’s content view, and if your cell’s “internal” height will not match (and probability of that is 99.99%), then it’s going to put errors in log.
How to avoid that error? I don’t know yet.
I post a question about that, and as soon as I find an answer, I will provide a link in this question.
Cell layout is not updated until cell is reused
If you want tableview to reflect changed cell layout.
After changing the cell Layout redraw the table view
tableView.beginUpdates()
tableView.setNeedsDisplay()
tableView.endUpdates()
For instance:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CustomCell else { return }
cell.collapseDescriptionLabel()
// redraw the tableView
tableView.beginUpdates()
tableView.setNeedsDisplay()
tableView.endUpdates()
}
You don't need to use layoutIfNeeded(), setNeedsLayout(), and layoutSubviews() to force the layout. You can use tableView.beginUpdates() and tableView.endUpdates().
For instance:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
tableView.beginUpdates()
cell.heightConstraint.constant = 50
tableView.endUpdates()
}
Scenario:
I have 2 VC -
ChildViewController
It has a tableView which displays a list of items. I need to pass the tableView.contentSize.height value, after the table is populated to my ParentVC. For that I am using delegate as
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableVieww.dequeueReusableCellWithIdentifier("cellreuse", forIndexPath: indexPath)
cell.textLabel?.text = "heyy"
hght.constant = tableVieww.contentSize.height
if flag == true
{
delegate.tableHeight(tableVieww.contentSize.height)
print(tableVieww.contentSize.height)
flag = false
}
return cell
}
ParentViewController
It has a tableView with one cell. This cell is showing view of a childVC i.e nwVC. I want to change the cell height depending upon the height of my ChildVC's tableView.
I am adding the childVC's view by the following code & I know this is the wrong place to do so but I am not getting how,where and what to do, to get the childViewController's function to be called before the ParentViewController's functions?
vc3 = self.storyboard?.instantiateViewControllerWithIdentifier("nwVC") as? nwVC//newVC is ChildViewController
vc3!.view!.frame = cell.myview.bounds
vc3!.didMoveToParentViewController(self)
cell.myview.addSubview(vc3!.view)//UIView inside the cell
vc3!.delegate=self
Problem -
The delegate methods of ParentViewController's tableView gets called before the childViewController's function's are called for which I cannot update my rowHeight as per the childVC's table content.
Finally,I figured out something that works but still I want suggestions from iOS dev's viewing this question.
Mark: I could not perform the loading of childVC's functions before the loading of ParentVC's table view delegate functions but I did something which works quite good.
In my ParentVC's
override func viewWillAppear(animated: Bool) {
vc3 = self.storyboard?.instantiateViewControllerWithIdentifier("nwVC") as? nwVC
addChildViewController(vc3!)
vc3!.didMoveToParentViewController(self)
vc3!.delegate=self
}
//childVC's delegate function implementation
func tableHeight(height: CGFloat) {
height = height//I get the table view height from the childVC,height is a variable declared as var height = 200.0(it can be any value > 0)
print(ht)
self.tableVIeww.reloadData()//reload my tableView
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return hgt//at first call it returns the default value but in the 2nd call it returns the value sent by childVC
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableVIeww.dequeueReusableCellWithIdentifier("cellreuse", forIndexPath: indexPath) as! myTVC
cell.backgroundColor = UIColor.greenColor()
vc3?.view.frame = cell.myview.bounds
cell.myview.addSubview((vc3?.view)!)//myView is the view in the cell's content view pinned to its edges.
return cell
}
Pro's & Con's
Pro's
The biggest advantage is that you get the works to be done.
Con's
As you can see that ChildVC's view is added 2 times (1 with the default cell size of height variable & the 2nd time when the table reloads). I feel that this might hamper the performance slightly & if Data is dynamic it might process for a bit long.
Please feel free to suggest...
I basically have a collection view where the cells have a view and that view's alpha decreases when the cell is tapped on. For some reason, when I scroll in the collection view, other cells views are also changing alphas and then the original cell that I selected also changed back. It has something to do with the cellForRowAtIndexPath method, but I'm not entirely sure what the issue is. Here is my code:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("chooseSpace", forIndexPath: indexPath)as! ChooseSpaceCell
let space = spaces2[indexPath.row]
cell.serviceLabel.text = spaces2[indexPath.row]
return cell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! ChooseSpaceCell
cell.mask.alpha = 0.7
}
func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! ChooseSpaceCell
cell.mask.alpha = 0.25
}
Originally, all the alphas start out at 0.25, change to 0.7 when tapped, and change back when deselected. This is a huge issue so any help would be much appreciated.
When you call
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("chooseSpace", forIndexPath: indexPath)as! ChooseSpaceCell
You are requesting a cell from the collection view. If there is a cell available it will reuse a cell that has already been created. If none are available it creates a new cell.
This means that when you scroll your collection view it is reusing the same cells that you used for previous items. If those items had there opacity changed the new items using that cell will have the same opacity.
You need to add an opacity field to your model or an attribute that will help you compute the opacity.
If I put a custom view in a cell , how can I notify tableview to update cell's height if my custom view's height has changed? I tried invalidateIntrinsicContentSize but that doesn't work. ReloadData works but I doubt whether using
self.superview?.superview?.superview as! UITableView).reloadData()
is a good implementation.
I have seen a similar question here but that's all about view from standard library. And this and this have no answer.
You are thinking in the right direction. However there are two problems with your approach:
reloadData reloads the whole table view and it does not animate the change.
Moving up the superview chain is bound to break when Apple changes the UITableView view hierarchy. They have done that before, so they might do it again.
To fix the first issue you should call reloadRowsAtIndexPaths:withRowAnimation: instead. That only reloads the cells that you specify in the indexPath array. So you pass it an array that only contains the indexPath of your cell.
The second issue is a bit trickier because a UITableViewCell has no reference to its UITableView (and it shouldn't). So it cannot tell the UITableView directly to reload the cell.
You can give each cell a closure that it should execute whenever its height has changed. So you just add a closure property in your custom cell:
class YourCustomTableViewCell: UITableViewCell {
var resizeClosure: (() -> Void)?
...
And you set this closure in your UITableViewControllerDataSource when you dequeue the cell:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("YourCustomCellIdentifier", forIndexPath: indexPath)
if let customCell = cell as? YourCustomTableViewCell {
customCell.resizeClosure = { [weak cell, weak tableView] in
if let currentIndexPath = tableView?.indexPathForCell(cell!) {
tableView?.reloadRowsAtIndexPaths([currentIndexPath], withRowAnimation: .Automatic)
}
}
}
return cell
}
Just make sure that you add tableView to the closure's capture list to avoid a strong reference cycle. That is done by adding the [weak tableView] to the closure.
And then when the cell changes its height you just execute the closure and the cell will be reloaded:
func someFunctionThatChangesTheHeight() {
// change the height
resizeClosure?()
}
I have a custom tableView, but the way it's designed makes the bottom and top part ugly when only half of the cell is visible. See picture for reference:
I want the bottom part (and the top after crolling) only visible when you can see 100% of the cell.
I tried this to check if the cells were completely visible, but I believe cellForRowAtIndexPath creates the reusableCells when it's partly visible and isn't called again when it's fully visible:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var myCell:ChooseStoryCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ChooseStoryCell
var cellRect = self.tableView.rectForRowAtIndexPath(indexPath)
var completelyVisible = CGRectContainsRect(self.tableView.bounds, cellRect)
if completelyVisible == true {
myCell.hidden = false
myCell.backgroundColor = Color.sharedColors().colorsArray[5]
myCell.storyLabel.text = stories[indexPath.row].name
myCell.circleView.layer.cornerRadius = 15
}
else{
myCell.hidden = true
}
How I would go forward with this?
Any help would be greatly appriciated!
Put your table view inside a parent UIView. A margin from top of tableview to top of parent view should be equal to what height of your cells is, same from bottom, the width of table view should be same as its parent's view, left and right margins equals to zero. So there is just extra space at top / bottom to display a cell.
On tableview set clipsToBounds to NO, on parent view make sure that clipsToBounds is set to YES.
The behavior should be like this, when scrolling the cell will be visible until it reaches the top boundary of that parent view and disappear at once.
Alternatively, you can also just reduce height of your tableview, move it down and set clipsToBounds to NO. It should do it. But I prefer to embed in View to be sure that nothing will be display outside.
Here is a code just came out of my mind. Not tested.
The basic idea is to see the intersection of the cell's frame and the table view's bounds and if the result is the cell's frame, then the cell frame is completely visible.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var myCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! UITableViewCell
var cellRect = tableView.rectForRowAtIndexPath(indexPath)
var completelyVisible = cellRect.rectByIntersecting(tableView.bounds) == cellRect
if completelyVisible == true {
myCell.contentView.hidden = false
}
else{
myCell.contentView.hidden = true
}
return myCell
}