UITableView row separator cut off when updating cell - ios

I have a grouped UITableView in which I display some car data in the second section. I load the image from a web server using SDWebImage. In its callback I resize the picture and update my image view.
However, as soon as I update my image view, the UITableView separator is cut off.
For illustration purposes I've given the respective elements background colors
When the image is not loaded yet, it looks like this:
When the image is updated, it looks like this
Notice that the row separator is somehow cut off between the UITableView cells even though no subview (the UIImageView) is hiding it.
Depending on the UITableView section the row height varies, so I've overwritten the heightForRowAtIndexPath UITableView delegate
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.section == 0 {
return GFFloat(44)
}
else {
return CGFloat(220)
}
}
Can someone tell me why the separators disappear and how I can fix this? I've read about reloading the next UITableViewCell when I update the image, but since I display a lot of cars, my app crashes when I try this.

Just add below method it will resolve separator issue.
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if cell.respondsToSelector("setSeparatorInset:") {
cell.separatorInset = UIEdgeInsetsZero
}
if cell.respondsToSelector("setPreservesSuperviewLayoutMargins:") {
cell.preservesSuperviewLayoutMargins = false
}
if cell.respondsToSelector("setLayoutMargins:") {
cell.layoutMargins = UIEdgeInsetsZero
}
}

Solution for Swift 5
Add this to your controller:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if cell.responds(to: #selector(setter: UITableViewCell.separatorInset)) {
cell.separatorInset = UIEdgeInsets.zero
}
if cell.responds(to: #selector(setter: UIView.preservesSuperviewLayoutMargins)) {
cell.preservesSuperviewLayoutMargins = false
}
if cell.responds(to: #selector(setter: UIView.layoutMargins)) {
cell.layoutMargins = UIEdgeInsets.zero
}
}

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.

Cannot work out vertically expandable UITableViewCells to show buttons

I have my UITableViewCells working fine. I want to be able to click the menu button to expand the height of the cell to reveal a set of buttons as per the image:
I have code as follows:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == Globals.selectedRowIndex {
return 335
} else {
return 125
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if indexPath.row == Globals.selectedRowIndex {
Globals.selectedRowIndex = -1
} else {
Globals.selectedRowIndex = indexPath.row
}
tableView.reloadData()
}
The buttons all get squished together on the smaller size though. I thought they would just be cropped out of the view. I think I can get around this by having them hide on click. I haven't tried this yet.
I also cannot set a height for them as my constraints seem to not be available for anything inside the cell. I don't know why. I can set autoresizing but not constraints.
I think though there's probably a completely better way to do this which I'm missing. Any pointers will be greatly appreciated.
First iterate upto tableCount and assign false. For Example,
for i=0;i<tablecount;i++
{
self.isSelected.append(false)
}
For Example, If you have 5 cells means,You will be having Values like this:
isSelected = [false, false, false, false, false]
Then, While Expanding Cells Assign True.Otherwise assign False
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedIndex[indexPath.row] = indexPath.row;
if isSelected[indexPath.row]
{
isSelected[indexPath.row] = false
self.tableView.reloadRowsAtIndexPaths([tableView.indexPathForSelectedRow!], withRowAnimation: .Fade)
}
else
{
isSelected[indexPath.row] = true
self.tableView.reloadRowsAtIndexPaths([tableView.indexPathForSelectedRow!], withRowAnimation: .Fade)
}
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
if isSelected[indexPath.row]
{
return 275.0
}
else
{
return 110.0
}
}

Adjust UITableViewCell separator line when in multiple edit mode

I'm looking to adjust the separator inset margin when in multiple edit mode in a standard UITableView in iOS 9/10.
My goal is to align the separator line with the left edge of the cell's text label.
In the past, I've removed the margins entirely by doing:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.separatorInset = .zero
cell.layoutMargins = .zero
}
That gives me a view such as:
If I try to increase the separator inset, the entire text label moves over, which I don't want. :
cell.separatorInset = UIEdgeInsetsMake(0, 40, 0, 0)
How can I just move the separator line, without affecting the text layout, and preferably without doing any overriding of layoutSubviews or drawing lines manually?
You can add separator manually, try to use below code
func viewDidLoad() {
self.tableview.separatorStyle = UITableViewCellSeparatorStyle.None
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let separator = CALayer()
separator.backgroundColor = UIColor.grayColor();
separator.frame = CGRectMake(0, 43, self.view.frame.size.width, 1);
cell.layer.insertSublayer(separator, atIndex:0)
return cell;
}
I'm on xcode 8, with ios 9
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
cell.layoutMargins = .zero
}
gives you the what you want as per your third picture.
I've tried
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
{
cell.layoutMargins = .zero
}
it works too, not sure why its not working on your side..

Changing UITableViewCell height using tableView.beginUpdates()

SO I want to change the height of my UITableViewCell by doing something like this:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedIndexPath = indexPath
tableView.beginUpdates()
tableView.endUpdates()
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath == selectedIndexPath {
return 140
} else {
return 90
}
}
However, the problem is that when I do this, the cell "expands" slower than the content of expanded portion shows, so whatever is in the expanded portion shows before the cell fully expands and it is kind of ugly. This problem can be fixed if I use tableView.reloadRowsAtIndexPaths(), but I don't want to reload the cell because I have a timer displayed in the expanded portion of the cell.

reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling

I feel like this might be a common issue and was wondering if there was any common solution to it.
Basically, my UITableView has dynamic cell heights for every cell. If I am not at the top of the UITableView and I tableView.reloadData(), scrolling up becomes jumpy.
I believe this is due to the fact that because I reloaded data, as I'm scrolling up, the UITableView is recalculating the height for each cell coming into visibility. How do I mitigate that, or how do I only reloadData from a certain IndexPath to the end of the UITableView?
Further, when I do manage to scroll all the way to the top, I can scroll back down and then up, no problem with no jumping. This is most likely because the UITableViewCell heights were already calculated.
To prevent jumping you should save heights of cells when they loads and give exact value in tableView:estimatedHeightForRowAtIndexPath:
Swift:
var cellHeights = [IndexPath: CGFloat]()
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? UITableView.automaticDimension
}
Objective C:
// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary = #{}.mutableCopy;
// declare table dynamic row height and create correct constraints in cells
tableView.rowHeight = UITableViewAutomaticDimension;
// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
[cellHeightsDictionary setObject:#(cell.frame.size.height) forKey:indexPath];
}
// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
if (height) return height.doubleValue;
return UITableViewAutomaticDimension;
}
Swift 3 version of accepted answer.
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 70.0
}
The jump is because of a bad estimated height. The more the estimatedRowHeight differs from the actual height the more the table may jump when it is reloaded especially the further down it has been scrolled. This is because the table's estimated size radically differs from its actual size, forcing the table to adjust its content size and offset.
So the estimated height shouldn't be a random value but close to what you think the height is going to be. I have also experienced when i set UITableViewAutomaticDimension
if your cells are same type then
func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 100//close to your cell height
}
if you have variety of cells in different sections then I think the better place is
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
//return different sizes for different cells if you need to
return 100
}
#Igor answer is working fine in this case, Swift-4 code of it.
// declaration & initialization
var cellHeightsDictionary: [IndexPath: CGFloat] = [:]
in following methods of UITableViewDelegate
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// print("Cell height: \(cell.frame.size.height)")
self.cellHeightsDictionary[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = self.cellHeightsDictionary[indexPath] {
return height
}
return UITableView.automaticDimension
}
I have tried all the workarounds above, but nothing worked.
After spending hours and going through all the possible frustrations, figured out a way to fix this. This solution is a life savior! Worked like a charm!
Swift 4
let lastContentOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastContentOffset, animated: false)
I added it as an extension, to make the code look cleaner and avoid writing all these lines every time I want to reload.
extension UITableView {
func reloadWithoutAnimation() {
let lastScrollOffset = contentOffset
beginUpdates()
endUpdates()
layer.removeAllAnimations()
setContentOffset(lastScrollOffset, animated: false)
}
}
finally ..
tableView.reloadWithoutAnimation()
OR you could actually add these line in your UITableViewCell awakeFromNib() method
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
and do normal reloadData()
I use more ways how to fix it:
For view controller:
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 70.0
}
as the extension for UITableView
extension UITableView {
func reloadSectionWithoutAnimation(section: Int) {
UIView.performWithoutAnimation {
let offset = self.contentOffset
self.reloadSections(IndexSet(integer: section), with: .none)
self.contentOffset = offset
}
}
}
The result is
tableView.reloadSectionWithoutAnimation(section: indexPath.section)
I ran into this today and observed:
It's iOS 8 only, indeed.
Overridding cellForRowAtIndexPath doesn't help.
The fix was actually pretty simple:
Override estimatedHeightForRowAtIndexPath and make sure it returns the correct values.
With this, all weird jittering and jumping around in my UITableViews has stopped.
NOTE: I actually know the size of my cells. There are only two possible values. If your cells are truly variable-sized, then you might want to cache the cell.bounds.size.height from tableView:willDisplayCell:forRowAtIndexPath:
You can in fact reload only certain rows by using reloadRowsAtIndexPaths, ex:
tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)
But, in general, you could also animate table cell height changes like so:
tableView.beginUpdates()
tableView.endUpdates()
Overriding the estimatedHeightForRowAtIndexPath method with an high value, for example 300f
This should fix the problem :)
Here's a bit shorter version:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.cellHeightsDictionary[indexPath] ?? UITableViewAutomaticDimension
}
There is a bug which I believe was introduced in iOS11.
That is when you do a reload the tableView contentOffSet gets unexpectedly altered. In fact contentOffset should not change after a reload. It tends to happen due to miscalculations of UITableViewAutomaticDimension
You have to save your contentOffSet and set it back to your saved value after your reload is finished.
func reloadTableOnMain(with offset: CGPoint = CGPoint.zero){
DispatchQueue.main.async { [weak self] () in
self?.tableView.reloadData()
self?.tableView.layoutIfNeeded()
self?.tableView.contentOffset = offset
}
}
How you use it?
someFunctionThatMakesChangesToYourDatasource()
let offset = tableview.contentOffset
reloadTableOnMain(with: offset)
This answer was derived from here
This one worked for me in Swift4:
extension UITableView {
func reloadWithoutAnimation() {
let lastScrollOffset = contentOffset
reloadData()
layoutIfNeeded()
setContentOffset(lastScrollOffset, animated: false)
}
}
One of the approach to solve this problem that I found is
CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock {
UIView.setAnimationsEnabled(true)
}
tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()
None of these solutions worked for me. Here's what I did with Swift 4 & Xcode 10.1...
In viewDidLoad(), declare table dynamic row height and create correct constraints in cells...
tableView.rowHeight = UITableView.automaticDimension
Also in viewDidLoad(), register all your tableView cell nibs to tableview like this:
tableView.register(UINib(nibName: "YourTableViewCell", bundle: nil), forCellReuseIdentifier: "YourTableViewCell")
tableView.register(UINib(nibName: "YourSecondTableViewCell", bundle: nil), forCellReuseIdentifier: "YourSecondTableViewCell")
tableView.register(UINib(nibName: "YourThirdTableViewCell", bundle: nil), forCellReuseIdentifier: "YourThirdTableViewCell")
In tableView heightForRowAt, return height equal to each cell's height at indexPath.row...
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
let cell = Bundle.main.loadNibNamed("YourTableViewCell", owner: self, options: nil)?.first as! YourTableViewCell
return cell.layer.frame.height
} else if indexPath.row == 1 {
let cell = Bundle.main.loadNibNamed("YourSecondTableViewCell", owner: self, options: nil)?.first as! YourSecondTableViewCell
return cell.layer.frame.height
} else {
let cell = Bundle.main.loadNibNamed("YourThirdTableViewCell", owner: self, options: nil)?.first as! YourThirdTableViewCell
return cell.layer.frame.height
}
}
Now give an estimated row height for each cell in tableView estimatedHeightForRowAt. Be accurate as you can...
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
return 400 // or whatever YourTableViewCell's height is
} else if indexPath.row == 1 {
return 231 // or whatever YourSecondTableViewCell's height is
} else {
return 216 // or whatever YourThirdTableViewCell's height is
}
}
That should work...
I didn't need to save and set contentOffset when calling tableView.reloadData()
I have 2 different cell heights.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
return Helper.makeDeviceSpecificCommonSize(cellHeight)
}
After I added estimatedHeightForRowAt, there was no more jumping.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
return Helper.makeDeviceSpecificCommonSize(cellHeight)
}
For me the working solution is
UIView.setAnimationsEnabled(false)
tableView.performBatchUpdates { [weak self] in
self?.tableView.reloadRows(at: [indexPath], with: .none)
} completion: { [weak self] _ in
UIView.setAnimationsEnabled(true)
self?.tableView.scrollToRow(at: indexPath, at: .top, animated: true) // remove if you don't need to scroll
}
I have expandable cells.
Try to call cell.layoutSubviews() before returning cell in func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. It's known bug in iOS8.
You can use the following in ViewDidLoad()
tableView.estimatedRowHeight = 0 // if have just tableViewCells <br/>
// use this if you have tableview Header/footer <br/>
tableView.estimatedSectionFooterHeight = 0 <br/>
tableView.estimatedSectionHeaderHeight = 0
I had this jumping behavior and I initially was able to mitigate it by setting the exact estimated header height (because I only had 1 possible header view), however the jumps then started to happen inside the headers specifically, not affecting the whole table anymore.
Following the answers here, I had the clue that it was related to animations, so I found that the table view was inside a stack view, and sometimes we'd call stackView.layoutIfNeeded() inside an animation block. My final solution was to make sure this call doesn't happen unless "really" needed, because layout "if needed" had visual behaviors in that context even when "not needed".
I had the same issue. I had pagination and reloading data without animation but it did not help the scroll to prevent jumping. I have different size of IPhones, the scroll was not jumpy on iphone8 but it was jumpy on iphone7+
I applied following changes on viewDidLoad function:
self.myTableView.estimatedRowHeight = 0.0
self.myTableView.estimatedSectionFooterHeight = 0
self.myTableView.estimatedSectionHeaderHeight = 0
and my problem solved. I hope it helps you too.
For me, it worked with "heightForRowAt"
extension APICallURLSessionViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
print("Inside heightForRowAt")
return 130.50
}
}
Actually I found if you use reloadRows causing a jump problem. Then you should try to use reloadSections like this:
UIView.performWithoutAnimation {
tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}

Resources