dequeueReusableCell breaks cliptobounds - ios

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.

Related

How to narrow UITableView cell height if there is dynamic structure in Swift?

I have a tableView and cells. The Cells are loaded from a xib and they have a label with automatic height. I need to narrow one cell if the user taps on it.
I have tried hiding - doesn't work
I have tried removeFromSuperView()- doesn't work
Is there any alternative?
When setting up your tableViewCell store the height anchor you want to update
var yourLabelHeightAnchor: NSLayoutConstraint?
private func setupLayout() {
yourLabelHeightAnchor = yourLabel.heightAnchor.constraint(equalToConstant: 50)
// Deactivate your height anchor as you want first the content to determine the height
yourLabelHeightAnchor?.isActive = false
}
When the user clicks on a cell, notify the tableView that the cell is going to change, and activate the height anchor of your cell.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: "YourTableViewCellIdentifier") as? YourCell
self.tableView.beginUpdates()
cell?.yourLabelHeightAnchor?.isActive = true
self.tableView.endUpdates()
}
Did you try to do something like this:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
var result: CGFloat
if (indexPath.row==0) {
result = 50 }
else {result = 130}
return result
}
This is just an example where height is changed for the first row. I tested on my application and it gave result like this.

UITableView jumps and flickers when scrolling

I have a UITableView that displays cells with an image and some text. The data is requested on demand - I first ask for data for 10 rows, then for then next 10 and so on. I do this in tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath). The problem is that when I receive the data and need to update the tableview it sometimes jumps and/or flickers. I make a call to reloadData. Here is part of the code:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
DispatchQueue.global(qos: .background).async {
if indexPath.row + 5 >= self.brands.count && !BrandsManager.pendingBrandsRequest {
BrandsManager.getBrands() { (error, brands) in
self.brands.append(contentsOf: brands as! [Brand])
DispatchQueue.main.async {
UIView.performWithoutAnimation {
self.brandsTableView.reloadData()
}
}
}
}
}
}
The height of the cells is constant returned like this:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 70
}
I am using Kingfisher to download and cache the images. Here is some more code from the datasource:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return brands.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.ImageTableCell, for: indexPath) as! ImageTableViewCell
let brand = brands[indexPath.row]
cell.centerLabel.text = brand.brand
cell.leftImageView.image = nil
if let url = BrandsManager.brandLogoURL(forLogoName: brand.logo!) {
let resource = ImageResource(downloadURL: url, cacheKey: url.absoluteString)
cell.leftImageView.kf.setImage(with: resource)
} else {
print("Cannot form url for brand logo")
}
return cell
}
How can I avoid the flickering and jumping of the table view on scroll? I looked at some of the similar questions but couldn't find a working solution for my case.
To remove the jumping issue you need to set estimatedHeightForRowAt the same as your row height. Assuming you will have no performance issues you can simply do the following:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.tableView(tableView, heightForRowAt: indexPath)
}
Or if the cell height is constant you can do tableView.estimatedRowHeight = 70.0.
Why this happens is because table view when reloading will use estimatedRowHeight for the cells that are invisible which results in jumping when the estimated height differs from the actual. To give you an idea:
Let's say that estimated height is 50 while the real height is 75. Now that you have scrolled down so that 10 cells are off the screen you have 10*75 = 750 pixels of content offset. No when reload occurs table view will ignore how many cells are hidden and will try to recompute that. It will keep reusing estimated row height until it finds the index path that should be visible. In this example it starts calling your estimatedHeightForRow with indexes [0, 1, 2... and increasing the offset by 50 until it gets to your content offset which is still 750. So that means it gets to index 750/50 = 15. And this produces a jump from cell 10 to cell 15 on reload.
As for the flickering there are many possibilities. You could avoid reloading the cells that don't need reloading by reloading only the portion of data source that has changed. In your case that means inserting new rows like:
tableView.beginUpdates()
tableView.insertRows(at: myPaths, with: .none)
tableView.endUpdates()
Still it seems strange you even see flickering. If only image flickers then the issue may be elsewhere. Getting an image like this is usually an asynchronous operation, even if the image is already cached. You could avoid it by checking if you really need to update the resource. If your cell is already displaying the image you are trying to show then there is no reason to apply the new resource:
if let url = BrandsManager.brandLogoURL(forLogoName: brand.logo!) {
if url != cell.currentLeftImageURL { // Check if new image needs to be applied
let resource = ImageResource(downloadURL: url, cacheKey: url.absoluteString)
cell.currentLeftImageURL = url // Save the new URL
cell.leftImageView.kf.setImage(with: resource)
}
} else {
print("Cannot form url for brand logo")
}
I would rather put this code into the cell itself though
var leftImageURL: URL {
didSet {
if(oldValue != leftImageURL) {
let resource = ImageResource(downloadURL: url, cacheKey: url.absoluteString)
leftImageView.kf.setImage(with: resource)
}
}
}
but this is completely up to you.
If you are appending data to the end of the tableView, do not call reloadData, which forces recalculation and redraw of all of the cells. Instead use UITableView.insertRows(at:with:) which will perform the appropriate insert animation if you use .automatic and leave the existing cells alone.

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
}

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

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()
}
}

Collection View Cells alpha is changing when it is not supposed to

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.

Resources