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?()
}
Related
I would like to confirm the approach I took to solve an issue with dequeuing custom cells in a UITableView as it scrolls such that the cells do not contain the old cell's data...
The app that contains a UITableView with custom UITableViewCells ("CustomCell"). Each CustomCell contains a UIStackView with one or more custom views via a nib ("CustomView"). I reuse the CustomCell as follows:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(ReuseIdentifierCustomCell, forIndexPath: indexPath) as! CustomCell
configureCell(cell, atIndexPath: indexPath)
return cell
}
The issue was that the cell would contain "old" data as the cell was being reused. To fix this, I override the prepareForReuse method in CustomCell as follows:
override func prepareForReuse() {
super.prepareForReuse()
for case let view as CustomView in stackView.subviews {
view.removeFromSuperview()
}
}
Then in layoutSubviews, I add the subviews back in:
override func layoutSubviews() {
super.layoutSubviews()
if stackView.subviews.isEmpty {
addCustomViewsToCell()
}
}
Performance seems fine thus far, but curious if this is a proper approach or if I will run into issues with scale in the future. I have not been able to find another workable approach thus far.
Thanks
Your code to reuse cells is correct. A common approach is to configure your cell's data within the cellForRowAtIndexPath function by setting a variable or calling a function on your custom cell:
let cell = tableView.dequeueReusableCellWithIdentifier(ReuseIdentifierCustomCell, forIndexPath: indexPath) as! CustomCell
cell.data = myData[indexPath.row] // where myData is an array of type [Data]
return cell
Your cell would be in charge of its own layout to display the new data:
var data: Data {
didSet {
// configure and refresh your UI here
}
}
I suspect your issue has to do with your configureCell function. If you can, move this code into your cell's logic instead. This will be cleaner and easier to understand.
As far as performance, you might be fine now if your stack views don't have much content in them, but if they continue to grow in complexity you might see frame rate drops on older devices.
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 have a UITableView with a transparent background color and cells which also have a transparent background color. When I reload my tableView with:
dataSource = some new data
tableView.reloadData()
I can see the new cells overlap the old ones.
I did try to use use
tableView.beginUpdates()
// remove all rows here
change data source
// insert new rows here
tableView.endUpdates()
but it did not work. I tried as well tableView.reloadRowsAtIndexPath(...) but still no luck.
And finally I set all my cells and my table view to clear graphic context when redrawn but it did not manage to fix this issue.
Any help would be greatly appreciated.
My cell creation function:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("suggestioncell")
cell.backgroundColor = UIColor.whiteColor().alpha(0.1)
cell.textLabel?.text = (suggestions![indexPath.row] as! SVPlacemark).formattedAddress
cell.clearsContextBeforeDrawing = true
cell.contentView.clearsContextBeforeDrawing = true
return cell
}
Try overriding prepareForReuse in you UITableViewCell subclass, and reset content there.
Here's what the documentation says about that:
Prepares a reusable cell for reuse by the table view's delegate.
If a UITableViewCell object is reusable—that is, it has a reuse identifier—this method is invoked just before the object is returned from the UITableView method dequeueReusableCellWithIdentifier:. For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state. The table view's delegate in tableView:cellForRowAtIndexPath: should always reset all content when reusing a cell. If the cell object does not have an associated reuse identifier, this method is not called. If you override this method, you must be sure to invoke the superclass implementation.
Custom UITableViewCell class:
class customCell: UITableViewCell {
override func prepareForReuse() {
self.textLabel?.text = nil
}
}
In your cellForRowAtIndexPath method:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("suggestionCell", forIndexPath: indexPath) as! customCell
cell.backgroundColor = UIColor.whiteColor().alpha(0.1)
cell.textLabel?.text = (suggestions![indexPath.row] as! SVPlacemark).formattedAddress
return cell
}
And, of course in your XIB/Storyboard, set the cell class to CustomCell, and set its reuse identifier.
I am trying to get a custom tableView cell with a textView inside working in my tableView. I have made a custom UITableViewCell with a textView inside it.
I can get the custom made UITableViewCell with the textView inside to appear in the UITableView.
I can click inside the textView to type something, but when I finish typing and click on another tableViewCell, the first tableViewCell with the textView inside disappears. After disappearing, it becomes an empty tableViewCell. XCode gives this message:
"no index path for table cell being reused"
However, when I scroll away in the tableView and scroll back to the empty tableViewCell, it reappears.
I don't know how to keep the tableViewCell from disappearing. It seems like the answer has something to do with using the restorationIdentifier inside of UITableView, but I'm not sure how to use it. In the docs, it says to use restorationIdentifier for state preservation.
Here is the relevant code I have:
inside ViewDidLoad():
tableView.registerClass(PhotoAndRateTableViewCell.classForCoder(), forCellReuseIdentifier: ReuseIds.reviewCell)
tableView.registerNib(UINib(nibName: "PhotoAndRateTableViewCell", bundle: NSBundle.mainBundle()), forCellReuseIdentifier: ReuseIds.reviewCell)
inside cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var section = indexPath.section
let cell = UITableViewCell()
var cell = tableView.dequeueReusableCellWithIdentifier(ReuseIds.reviewCell, forIndexPath: indexPath) as PhotoAndRateTableViewCell
cell.selectionStyle = UITableViewCellSelectionStyle.None
return cell
}
You're misusing the dequeue procedure:
var cell = tableView.dequeueReusableCellWithIdentifier(ReuseIds.reviewCell, forIndexPath: indexPath) as? PhotoAndRateTableViewCell
if (cell == nil) {
cell = PhotoAndRateTableviewCell();
}
You want to reuse a cell if available, or create a new one if not. In your case, you're creating a cell every time (of the generic class) and then attempting to dequeue a cell from your custom class (which has never been created)
As far as preserving the data, you need to implement the prepareForReuse method in the table cell which should clear whatever index specific data was contained in the cell. Then in cellForRow you can re-set the data for the cell for re-appearance
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.