129
votes

I have some strange issue with UITableView only in iOS 7.

UITableViewCellSeparator disappears above the first row and below the last row. Sometimes after selecting the rows or some scrolling actions it appears.

In my case tableView is loaded from the Storyboard with UITableViewStylePlain style. The problem is surely not in UITableViewCellSeparatorStyle, which is not changed from default UITableViewCellSeparatorStyleSingleLine.

As I read at Apple Dev Forums (here and here) other people have such problem and some workarounds are found, for example:

Workaround: disable the default selection and recreate the behaviour in a method
trigged by a tapGestureRecognizer.

But I am still searching for the reason of such separator strange behaviour.

Any ideas?

Update: As I saw in XCode 5.1 DP and iOS 7.1 beta, Apple tried to fix this problem. Now separator is shown as needed sometimes below the last row, after some refreshing, but not after tableview creation.

30
Sometimes I feel like iOS7 is so immature. This and the long list of things that I have here show how far iOS7 is from perfection of previous iOSs.Bms270
The Settings app by Apple has the same problem, so I believe this is an iOS 7 issue. I have reported it, and in their followup they asked for images showing the issue.Johan
@Gatada Where in the Settings app are you seeing the issue?Jamie Forrest
Encountered the same issue and fixed by adding [cell setSeparatorInset:UIEdgeInsetsFromString(@"1")]; with in (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath methodAyesha Fatima
This still appears to be broken in the iOS 8.0 betas. I've opened radar 17724043 should anyone care to dup it.Andy Wilkinson

30 Answers

77
votes

I dumped the subview hierarchy of affected cells and found that the _UITableViewCellSeparatorView was set to hidden. No wonder it's not shown!

I overrode layoutSubviews in my UITableViewCell subclass and now the separators are displayed reliably:

Objective-C:

- (void)layoutSubviews {
    [super layoutSubviews];

    for (UIView *subview in self.contentView.superview.subviews) {
        if ([NSStringFromClass(subview.class) hasSuffix:@"SeparatorView"]) {
            subview.hidden = NO;
        }
    }
}

Swift:

override func layoutSubviews() {
    super.layoutSubviews()

    guard let superview = contentView.superview else {
        return
    }
    for subview in superview.subviews {
        if String(subview.dynamicType).hasSuffix("SeparatorView") {
            subview.hidden = false
        }
    }
}

The other solutions proposed here didn't work consistently for me or seem clunky (adding custom 1 px footer views).

42
votes

This worked for me:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    // fix for separators bug in iOS 7
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
12
votes

I also had the problem with missing separator and I found out that the problem only occured when heightForRowAtIndexPath was returning a decimal number. Solution:

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return ceil(yourHeight) // Ceiling this value fixes disappearing separators
}
11
votes

Did you try adding a UIView of height 1 in the header and the footer of the table with light gray background color? Basically it'll mock the first and last separators.

8
votes

@samvermette

I fixed the problem by using this delegate methods. Now it doesn't flicker:

-(void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
    // fix for separators bug in iOS 7
    tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
}


-(void)tableView:(UITableView *)tableView didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath {
    // fix for separators bug in iOS 7
    tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
}
7
votes

We encountered this issue in our app. When the user selected a cell, a new table view was pushed onto the navigation controller stack and then when the user popped it off, the separator was missing. We solved it by putting [self.tableView deselectRowAtIndexPath:indexPath animated:NO]; in the didSelectRowAtIndexPath table view delegate method.

6
votes

Here is an easier (although a bit messy) workaround if you need one, try selecting and deselecting the cell after the data is reloaded and the default animation is done.

just add:

[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[self.tableView deselectRowAtIndexPath:indexPath animated:NO];
6
votes

I've found that the easiest solution is to, after reloading a cell, also reload the cell above:

if (indexPath.row > 0) {
    NSIndexPath *path = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:indexPath.section];
    [self.tableView reloadRowsAtIndexPaths:@[path] withRowAnimation:UITableViewRowAnimationNone];
}
4
votes

A simple and clean solution that works on both iOS 8 and iOS 9 (beta 1)

Here is a simple, clean and non-intrusive workaround. It involves calling a category method that will fix the separators.

All you need to do is to walk down the hierarchy of the cell and un-hide the separator. Like this:

for (UIView *subview in cell.contentView.superview.subviews) {
    if ([NSStringFromClass(subview.class) hasSuffix:@"SeparatorView"]) {
        subview.hidden = NO;
    }
}

What I recommend is to add this to a category on UITableViewCell, like this:

@interface UITableViewCell (fixSeparator)
- (void)fixSeparator;
@end

@implementation UITableViewCell (fixSeparator)

- (void)fixSeparator {
    for (UIView *subview in self.contentView.superview.subviews) {
        if ([NSStringFromClass(subview.class) hasSuffix:@"SeparatorView"]) {
            subview.hidden = NO;
        }
    }
}

@end

Because the separator can disappear in different cell than the one currently selected, it's probably a good idea to call this fix on all cells in the table view. For that, you can add a category to UITableView that goes like this:

@implementation UITableView (fixSeparators)

- (void)fixSeparators {
    for (UITableViewCell *cell in self.visibleCells) {
        [cell fixSeparator];
    }
}

@end

With this in place, you can call -fixSeparatos on your tableView right after the action that causes them to disappear. In my case, it was after calling [tableView beginUpdates] and [tableView endUpdates].

As I stated at the beginning, I've tested this on both iOS 8 and iOS 9. I presume it will work even on iOS 7 but I don't have a way to try it there. As you are probably aware, this does fiddle with internals of the cell so it could stop working in some future release. And Apple could theoretically (0.001% chance) reject your app because of this, but I can't see how they could even find out what you are doing there (checking the suffix of a class can't be detected by static analyzers as something bad, IMO).

4
votes

Based on @ortwin-gentz's comment, this solution works for me in iOS 9.

func fixCellsSeparator() {

    // Loop through every cell in the tableview
    for cell: UITableViewCell in self.tableView.visibleCells {

        // Loop through every subview in the cell which its class is kind of SeparatorView
        for subview: UIView in (cell.contentView.superview?.subviews)!
            where NSStringFromClass(subview.classForCoder).hasSuffix("SeparatorView") {
                subview.hidden = false
        }
    }

}

(Swift code)

I use fixCellsSeparator() function after calling endUpdates() in some methods of my tableView, for example:

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

    //  Perform some stuff
    //  ...

    self.tableView.endUpdates()
    self.fixCellsSeparator()

}

I hope this solution will be helpfull for someone!

2
votes

Complement to the answer of airpaulg.

So basically one has to implement two UITableDelegate methods. Here's my solution which works on both iOS7 and iOS6.

#define IS_OS_VERSION_7 (NSFoundationVersionNumber_iOS_6_1 < floor(NSFoundationVersionNumber))

#define UIColorFromRGB(hexRGBValue) [UIColor colorWithRed:((float)((hexRGBValue & 0xFF0000) >> 16))/255.0 green:((float)((hexRGBValue & 0xFF00) >> 8))/255.0 blue:((float)(hexRGBValue & 0xFF))/255.0 alpha:1.0]

// This will hide the empty table grid below the cells if they do not cover the entire screen

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
    UIView *view = nil;

    if (IS_OS_VERSION_7 /* && <is this the last section of the data source> */)
    {
        CGFloat height = 1 / [UIScreen mainScreen].scale;
        view = [[UIView alloc] initWithFrame:CGRectMake(0., 0., 320., height)];
        view.backgroundColor = UIColorFromRGB(0xC8C7CC);
        view.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    }
    else
    {
        view = [UIView new];
    }
    return view;
}


- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
    if (IS_OS_VERSION_7 /* && <is this the last section of the data source> */)
    {
        return 1 / [UIScreen mainScreen].scale;
    }
    else
    {
        // This will hide the empty table grid below the cells if they do not cover the entire screen
        return 0.01f;
    }
}
2
votes

This fixed the issue for me:

Make sure clipsToBounds is set to YES for the cell, but NO for the cell's contentView. Also set cell.contentView.backgroundColor = [UIColor clearColor];

2
votes

Seems like this issue manifests under so many circumstances.

For me it had something to do with cell selection. I have no idea why and didn't have time to dig into it too deep, but I can say it started occurring when I set the cell's selectionStyle to none. ie:

//This line brought up the issue for me
cell.selectionStyle = UITableViewCellSelectionStyleNone;

I tried using some of the delegate methods above that toggle on and off the separatorStyle property of the tableView but they didn't seem to do anything to fix my issue.

The whole reason why I needed that was because I didn't even need cell selection.

So, I did find something that worked for me. I just disabled selection on the UITableView:

tableView.allowsSelection = NO;

Hope this helps someone, if you don't need to have cell selection.

2
votes

I tried so many suggestions to fix but cannot fix that.Finally I decided to custom separator line as below:

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    var lineView = UIView(frame: CGRectMake(20, cell.contentView.frame.size.height - 1.0, cell.contentView.frame.size.width - 20, 1))

    lineView.backgroundColor = UIColor(red: 170.0/255.0, green: 170.0/255.0, blue: 170.0/255.0, alpha: 1)
    cell.contentView.addSubview(lineView)
}

P/S: custom at willDisplayCell NOT cellForRowAtIndexPath

1
votes

Quite surprisingly, changing cell's separatorInset value back and forth seems to be working:

NSIndexPath *selectedPath = [self.controller.tableView indexPathForSelectedRow];
[self.controller.tableView deselectRowAtIndexPath:selectedPath animated:YES];

UITableViewCell *cell = [self.controller.tableView cellForRowAtIndexPath:selectedPath];
UIEdgeInsets insets = cell.separatorInset;
cell.separatorInset = UIEdgeInsetsMake(0.0, insets.left + 1.0, 0.0, 0.0);
cell.separatorInset = insets;
1
votes

I also had the problem of this inconsistent separator line display at the bottom of my UITableView. Using a custom footer view, I have been able to create a similar line and to display it on top of the potential existing one (which was sometimes missing).

The only problem of this solution is that Apple may change one day the thickness of the line

- (UIView*) tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
    float w = tableView.frame.size.width;

    UIView * footerView =  [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, 1)];
    footerView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    footerView.clipsToBounds=NO;

    UIView* separatoraddon = [[UIView alloc] initWithFrame:CGRectMake(0, -.5, w, .5)];
    separatoraddon.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    separatoraddon.backgroundColor = tableView.separatorColor;
    [footerView addSubview:separatoraddon];

    return footerView;
}
- (CGFloat) tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
    return 1;
}
1
votes

I've resolved putting these lines of code where the update of the tableview hapens:

self.tableView.separatorStyle = UITableViewCellSeparatorStyle.None;
self.tableView.separatorStyle = UITableViewCellSeparatorStyle.SingleLine;

For example, in my case i've putted them here:

tableView.beginUpdates()
tableView.insertRowsAtIndexPaths(insertIndexPaths, withRowAnimation: UITableViewRowAnimation.Fade)
tableView.endUpdates()
self.tableView.separatorStyle = UITableViewCellSeparatorStyle.None;
self.tableView.separatorStyle = UITableViewCellSeparatorStyle.SingleLine;
1
votes

You need to remove a cell selection before you doing cell's update. Then you could restore selection.

NSIndexPath *selectedPath = [self.tableview indexPathForSelectedRow];
    [self.tableview deselectRowAtIndexPath:selectedPath animated:NO];
    [self.tableview reloadRowsAtIndexPaths:@[ path ] withRowAnimation:UITableViewRowAnimationNone];
    [self.tableview selectRowAtIndexPath:selectedPath animated:NO scrollPosition:UITableViewScrollPositionNone];
1
votes

To fix the above issue, add empty footer in viewDidLoad.

UIView *emptyView_ = [[UIView alloc] initWithFrame:CGRectZero];   
emptyView_.backgroundColor = [UIColor clearColor];  
[tableView setTableFooterView:emptyView_];

Please don't use the above lines in viewForFooterInSection delegate method. Means Dont implement viewForFooterInSection method.

0
votes

to expand on airpaulg answer, because it didn't entirely work for me..

i had to also implement the heightforfooter method to get the height

then i noticed the lightgraycolor is too dark. i fixed this by grabbing the current separator color of the tableview and using that:

UIColor *separatorGray = [self.briefcaseTableView separatorColor]; [footerSeparator setBackgroundColor:separatorGray];

0
votes

I also ran into this problem in our project. My table view had a tableFooterView which could make this happen. I found the separator below the last row would appear if I removed that tableFooterView.

0
votes

What made the difference for me was reloading the row for which the bottom separator line would not appear:

NSIndexPath *indexPath = 
     [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex];  
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
0
votes

I solve this problem in another way: add a layer that its height is 0.5px and its color is lightgray into tableview.tableFooterView as its sublayer.

the code is just like this:

UIView *tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 70)];
CALayer *topSeperatorLine = [CALayer layer];
topSeperatorLine.borderWidth = 0.5f;
topSeperatorLine.borderColor = [UIColor lightGrayColor].CGColor;
topSeperatorLine.frame = CGRectMake(0, 0, 320, 0.5f);
[tableFooterView.layer addSublayer:topSeperatorLine];
self.tableView.tableFooterView = tableFooterView;
0
votes

In your UITableViewCell Subclass implement layoutSubviews and add:

- (void)layoutSubviews{
    [super layoutSubviews]
    for (UIView *subview in self.contentView.superview.subviews) {
        if ([NSStringFromClass(subview.class) hasSuffix:@"SeparatorView"]) {
            CGRect separatorFrame = subview.frame;
            separatorFrame.size.width = self.frame.size.width;
            subview.frame = separatorFrame;
        }
    }
}
0
votes

As someone else mentioned, this problem seems to manifest in a variety of ways. I resolved my issue via the following workaround:

[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
0
votes

Going through the answers and solutions I came up with such observations:

This seems to be problematic in both iOS 7 and 8. I noticed the problem when selecting cell from code in viewDidLoad method so:

1) workaround was doing so in viewDidAppear: if someone doesn't mind the noticeable delay between presenting the view and selecting the cell

2) the second solution worked for me but the code looks a bit fragile as it base on the internal implementation of the UITableViewCell

3) adding own separator seems to be the most flexible and best for now but require more coding :)

0
votes

Setting the style in viewWillAppear worked for me.

0
votes

As this is still an issue with IOS 8, I will add my solution in Swift. Set the tableview separator line to none. Then add this code to the cellForRowAtIndexPath delegate method. It will add a nice separator. The if statement allows to decide which cells should have a separator.

    var separator:UIView!
    if let s = cell.viewWithTag(1000)
    {
        separator = s
    }
    else
    {
        separator = UIView()
        separator.tag = 1000
        separator.setTranslatesAutoresizingMaskIntoConstraints(false)
        cell.addSubview(separator)

        // Swiper constraints
        var leadingConstraint = NSLayoutConstraint(item: separator, attribute: .Leading, relatedBy: .Equal, toItem: cell, attribute: .Leading, multiplier: 1, constant: 15)
        var heightConstraint = NSLayoutConstraint(item: separator, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 0.5)
        var bottomConstraint = NSLayoutConstraint(item: cell, attribute: .Bottom, relatedBy: .Equal, toItem: separator, attribute: .Bottom, multiplier: 1, constant:0)
        var trailingConstraint = NSLayoutConstraint(item: cell, attribute: .Trailing, relatedBy: .Equal, toItem: separator, attribute: .Trailing, multiplier: 1, constant: 15)
        cell.addConstraints([bottomConstraint, leadingConstraint, heightConstraint, trailingConstraint])
    }

    if indexPath.row == 3
    {
        separator.backgroundColor = UIColor.clearColor()
    }
    else
    {
        separator.backgroundColor = UIColor.blackColor()
    }
0
votes

Here's my solution to this problem. After you insert your cell(s), reload the section. If reloading the entire section is too intensive for you, just reload the indexPaths above and below.

[CATransaction begin];
[CATransaction setCompletionBlock:^{
    //  Fix for issue where seperators disappear

    [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];
}];

[self.tableView insertRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationFade];

[CATransaction commit];
0
votes

ran into a similar problem, I found that another good solution, especially if your dataSource is not large is reload the tableData when you implement the -(void)scrollViewDidScroll:(UIScrollView *)scrollView method, here's an example:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
      if (scrollView == self.tableView1) {
          [self.tableView1 reloadData];
      }

      else {
          [self.tableView2 reloadData];
      }
}

You can also just reload non-visible data based on the visible dataSource, but that requires some more hacking.

Keep in mind this delegate function falls under the UITableViewDelegate protocol!

Hope it helps!