Why is cellForRowAtIndexPath being called when the cell is not visible? - ios

I have a UITableViewController sitting inside a Container which sits inside another UIViewController. I built it in a storyboard.
Here is a snap. The UITableViewController in question is on the right-hand side and is called LayerTableViewController.
For some reason, the cellForRowAtIndexPath method of this UITableView is being called for cells which are not currently visible. I tested this by changing the method to the following:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("layerCell", forIndexPath: indexPath) as! LayerTableViewCell
// Configure the cell...
cell.layerCircleView.layer.cornerRadius = cell.layerCircleView.layer.bounds.width / 2
let label = UILabel(frame: CGRect(x: cell.layerCircleView.frame.midX - 10, y: cell.layerCircleView.frame.midY - 10, width: 20, height: 20))
label.text = String(indexPath.row)
cell.layerCircleView.addSubview(label)
print("indexPath.row: \(indexPath.row)")
return cell
}
This code yielded the following odd result. In the console, numbers are being printed occasionally out of order. Cells which are not remotely near the visible screen are having their indexPath.row passed in to this method. Furthermore, the UILabels that I'm making are all being rendered on top of one another.
Here is a combined image of the simulator and terminal output.
As you can see, there is a random line that says indexPath.row: 0 when the indexPath.rows are sitting at around the mid twenties.
So to sum up, why is there a call for cellForRowAtIndexPath with an indexPath.row that isn't currently visible, and why are the numbers rendering on top of one another?

So I'm still not sure as to why there were random calls to cellForRowAtIndexPath for cells that weren't visible, but that doesn't seem to be a big issue. I thought that and the overlaid rendering were connected but they weren't.
The issue with the overlaid rendering is that all the UILabels were being added to a subview of the UITableViewCell, namely cell.layerCircleView. So as I discovered, when the cell is reused, so too is the reference to the layerCircleView. So all I had to do to fix it was clean up the view once it left the screen:
override func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let cell = cell as! LayerTableViewCell
for subview in cell.layerCircleView.subviews {
subview.removeFromSuperview()
}
}

Related

Dynamic height cells with a jerky scrolling

I know this is quite a hot topic answered in several places but no answer yet solved my problem. I'm working on an app with a main tableview with multiple cell types (table can be up to hundreds of cells), each one has a different potential height, depends on its content. I'm trying to rely more on dynamic cell heights, calculated by the system when drawing the cell but my scrolling is badly affected when i'm trying to scroll to the bottom of the table.
I understand the estimated height of a cell should be really close to what it is eventually, but there is no way to do that unless I manually calculate the size of each cell by summing up all of its texts, images, constraints and so on... That pretty much knocks the edge out of using dynamic cell heights, doesn't it?
The best solution i've found online is caching up the real cell heights on "cellWillDisplay", but it only works after all cells are presented at least once.
Thing is, when my app loads, it automatically scrolls to bottom without animation so "cellWillDisplay" isn't called for all cells above.
This is my estimatedHeightForRow snippet:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let object = self.fetchedResultsController?.object(at: indexPath) as? Object {
if let objectID = object.objectIDPermanentString {
if let savedHeight = self.rowsHeights[objectID] {
return savedHeight
}
}
}
return UITableViewAutomaticDimension
}
This is my cellForRow method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let object = self.fetchedResultsController?.object(at: indexPath)
let cell = tableView.dequeueReusableCell(withIdentifier: object.id, for: indexPath)
self.configureCell(cell: cell, object: object)
return cell
}

dequeueReusableCell breaks cliptobounds

I've got a very simple UIScrollView with some content (many subviews). This scroll view is used to show some posts made by users (image + text). One of these views is actually the image of the author and it overflows bottom cell bounds. It is thus overlapped with the cell coming after, and using clipToBounds = false I'm able to obtain the desired result. Everything works great if I scroll down. When I start to scroll back up the view that previously was overlying now gets clipped.
Cell overlapping working fine
Cell overlapping not working (when I scroll up)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = (indexPath.row % 2 == 0) ? "FeedCellLeft" : "FeedCellRight";
let cell = feedScrollView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! FeedCell;
self.setUpCell(cell, atIndexPath: indexPath);
return cell
}
the setUpCell function simply perform some UI related tasks
let row = indexPath.row
cell.postImage.downloadImageFrom(link: rows[row].image, contentMode: .scaleToFill)
cell.postAuthorImage.downloadImageFrom(link: "https://pbs.twimg.com/profile_images/691867591154012160/oaq0n2zy.jpg", contentMode: .scaleToFill)
cell.postAuthorImage.layer.cornerRadius = 22.0;
cell.postAuthorImage.layer.borderColor = UIColor.white.cgColor
cell.postAuthorImage.layer.borderWidth = 2.0;
cell.postAuthorImage.layer.masksToBounds = true;
cell.selectionStyle = .none
cell.postData.layer.cornerRadius = 10.0;
cell.contentView.superview?.clipsToBounds = false;
cell.clipsToBounds = false;
if (indexPath.row % 2 != 0) {
cell.postData.transform = CGAffineTransform.init(rotationAngle: (4 * .pi) / 180);
} else {
cell.postData.transform = CGAffineTransform.init(rotationAngle: (-4 * .pi) / 180);
}
It seems that the deque operation breaks the layout I've made (using autolayout). I've tried many solution like this
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.contentView.superview?.clipsToBounds = false;
cell.clipsToBounds = false;
cell.contentView.clipsToBounds = false;
}
But the results looks always the same. The height of every row is fixed.
I think the issue is with the hierarchy of subviews. When you scroll down, you cells dequeued from top to bottom and added to UITableView in the same order and all looks fine. Because the previous cell is above the following in view hierarchy.
But when you scroll up, cells are dequeued from bottom to top and it means that the cell on top is "behind" the previous cell. You can easily check it with Debugging View Hierarchies feature for Xcode.
You can try to bringSubviewToFront: for example:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.superview.bringSubview(toFront cell)
}
Updated version
I have made small research in Playgrounds and found only one reasonable option to implement overlapping cells without huge performance issues. The solution is based on cell.layer.zPosition property and works fine (at least in my Playground). I updated the code inside willDisplay cell: with the following one:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.layer.zPosition = (CGFloat)(tableView.numberOfRows(inSection: 0) - indexPath.row)
}
According to the documentation for .zPosition (Apple Developer Documentation):
The default value of this property is 0. Changing the value of this property changes the the front-to-back ordering of layers onscreen. Higher values place this layer visually closer to the viewer than layers with lower values. This can affect the visibility of layers whose frame rectangles overlap.
So I use current dataSource counter as minuend and indexPath.row of the current cell as subtrahend to calculate zPosition of the layer for each cell.
You can download full version of my playground here.

Pass message from ChildViewController toParentViewController before ParentViewController's delegate methods are called

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...

UITableView with fading cells on top and bottom - swift

I need my table view to fade its top and bottom cells as it is scrolled. I have tried some solutions involving gradient and masks but non of it worked, the gradient from clear to white has a black tint. Does anyone has a solution to accomplish that in swift?
You can achieve desired effect by using some methods defined in UITableViewDelegate protocol. First thing you need to know that cell main subview is contentView add all other subviews are subviews of it. What you need to do is to set contentView alpha to 0 in cellForRowAtIndexPath:indexPath: method.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel?.text = "Fade Cell"
cell.contentView.alpha = 0 // Here we set the alpha of the content view
return cell
}
If you run your application now, you would have plain white cells. Only thing we need now is to know when cells are displayed, so we can show contentView. Second UITableViewDelegate protocol method comes in handy now.
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
UIView.animateWithDuration(0.4) {
cell.contentView.alpha = 1
}
}
This delegate method is called when cells are preparing to be displayed, and it's the perfect place to animate contentView alpha property to 1.

Swift UITableView didSelectRowAtIndexPath bug

I have a UITableView with the subtitles hidden but set up where when someone selects a cell it shows that cell's subtitle. This works fine except that after tapping any cell to reveal its subtitle if you scroll down you will find that every 12 cells have their subtitle unhidden (as well as the one it was supposed to reveal). Here is the code I'm using in didSelectRowAtIndexPath:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
for cell in tableView.visibleCells() {
cell.detailTextLabel??.hidden = true
}
var cell = tableView.cellForRowAtIndexPath(indexPath)
cell?.detailTextLabel?.hidden = false
}
I'm sure this is related to ".visibleCells()" since every 12 cells is about the height of my visible table on my iPhone 6 Plus. When I run it on a 4s in the simulator it's about every 8 cells. But I'm not sure how else to do it besides 'visibleCells'? But it's strange because it's the whole table - all the way down, every 12 cells is showing its subtitle...
thanks for any help
UITableView reuses its cells. So the cell for row a row you clicked on (unhidden the subtitle) may be used for row another row.
The solution is to define prepareForReuse() method in the UITableViewCell subclass (or make the subclass if you do not have one) and hide the subtitle again there.
Add that dataSource's method to your controller. Should work fine.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) {
var identifier = "cellIdentifier"
var cell = tableView. dequeueReusableCellWithIdentifier(identifier, forIndexPath: indexPath)
cell.detailTextLabel?.hidden = true
return cell
}

Resources