4

I have a problem with the default separator on UITableView in iOS 7.

When used as default the first and last separators have no insets, others are a bit inset. The original situation can be seen below:

Everything is OK

Everything is ok. The first and last separators spread through the entire width of the table while the others are a bit smaller. Now I have the table view set to editing and I allow the user to reorder cells. And when the user does so the separators get messed up and do not appear correctly. The situation can be seen on the images below:

enter image description here enter image description here

Do I really need to reload the data in order to fix this issue or is it an iOS 7 bug or am I doing something wrong?

How to fix this?

EDIT
Added some info about my implementation. I return NO on - (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath and UITableViewCellEditingStyleNone on - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath. My - (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath is:

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (!cell)
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];

    cell.shouldIndentWhileEditing = NO;
    cell.textLabel.font = [UIFont someFont];

    UIColor *color = [UIColor randomColor];
    cell.textLabel.text = @"Some text";

    CGRect rect = CGRectMake(0, 0, 15, 15);

    UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0);

    CGContextRef ctx = UIGraphicsGetCurrentContext();

    [color set];
    CGContextFillEllipseInRect(ctx, rect);

    cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return cell;
}

and

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
    if (sourceIndexPath.row == destinationIndexPath.row) return;

    NSString *tmp = [itemOrder objectAtIndex:sourceIndexPath.row];
    [itemOrder removeObjectAtIndex:sourceIndexPath.row];
    [itemOrder insertObject:tmp atIndex:destinationIndexPath.row];
    didReorder = YES;
}
Majster
  • 3,611
  • 5
  • 38
  • 60
  • it happens cause you are recycling the cells, you would need to reload data first :) – Calleth 'Zion' Feb 13 '14 at 20:36
  • Not sure if I fully understand you, when am I supposed to reload data? Is that good practice? Also which is better? To use a new cell each time or to reload data? – Majster Feb 13 '14 at 20:40
  • please, post your table view delegate code – vokilam Feb 13 '14 at 20:46
  • When you are moving the cell the recycled one is moved, so it moves the entire cell with the differente separator, you need to reload data after moving the cells so the large separators stay into the correct place... – Calleth 'Zion' Feb 13 '14 at 21:17
  • If I reload the data immediately after the `moveRowAtIndexPath` gets called it flicks the cell and it looks ugly. If I do the `performSelector: withObject: afterDelay:` it feels like I'm hacking. Is this really the proper way to fix this issue? – Majster Feb 13 '14 at 21:33
  • @Majster check my answer.that's perfect and short solution for that. – Darshan Kunjadiya Feb 14 '14 at 05:13
  • how do you instantiate the 1st cell with long separator? – vokilam Feb 14 '14 at 06:23
  • 1
    FYI: this bug has been **fixed in iOS 8**. – Jonathan Sep 27 '14 at 21:57

2 Answers2

2

This seems to be a problem in the iOS SDK. One way to solve it would be to manually apply the correct separator insets to all cells. However, I personally prefer to let the system do that.

If you reload a cell after reordering, the correct separator insets will be applied by the system. Therefore, you should simply make sure to reload the relevant cells after reordering.

Fix for user reorderings

There's a private method tableView:didEndReorderingRowAtIndexPath: that's called on your table view's delegate after the reordering (animation) has ended. In this method you should reload the cells:

- (void)tableView:(UITableView *)tableView didEndReorderingRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section]
                  withRowAnimation:UITableViewRowAnimationNone];
}

This fix only works if rows are only moved within a section (and not between sections).

(It would be more efficient to only reload the moved cell and the cell at the old location using the method reloadRowsAtIndexPaths:withRowAnimation:. However, you'd have to store the old location before the cell is moved. which would be more complex to implement. For small sections/simple cells reloading the whole section might be a little bit easier.)

Fix for programmatic moves

Unfortunately, the private delegate method used in the previous fix is only called after reorderings by the user, and not after programmatically moving a row (using moveRowAtIndexPath:toIndexPath:). Therefore, we'll have to use another fix for that.

Since table views internally use CALayer animations, we can use a CATransaction to get to know when the animation has ended. (You do need to add QuartzCore.framework to your project for this.) My fix looks like this:

UITableView+NicelyMoves.h

#import <UIKit/UIKit.h>

@interface UITableView (NicelyMoves)

- (void)nicelyMoveRowAtIndexPath:(NSIndexPath *)indexPath
                     toIndexPath:(NSIndexPath *)newIndexPath;

@end

UITableView+NicelyMoves.m

#import <QuartzCore/QuartzCore.h>
#import "UITableView+NicelyMoves.h"

@implementation UITableView (NicelyMoves)

- (void)nicelyMoveRowAtIndexPath:(NSIndexPath *)indexPath
                     toIndexPath:(NSIndexPath *)newIndexPath
{
    [CATransaction begin];

    [CATransaction setCompletionBlock:^{
        // This is executed after the animations have finished
        [self reloadRowsAtIndexPaths:@[indexPath, newIndexPath]
                    withRowAnimation:UITableViewRowAnimationNone];
    }];

    [self moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];

    [CATransaction commit];
}

@end

Usage

Now, instead of using moveRowAtIndexPath:toIndexPath:, you should use the custom method nicelyMoveRowAtIndexPath:toIndexPath:. For example, you could do this:

#import "UITableView+NicelyMoves.h"

@implementation YourTableViewController

- (void)moveTheRow
{
    // Move row 0 to 2 in section 0
    NSIndexPath *from = [NSIndexPath indexPathForRow:0 inSection:0];
    NSIndexPath *to = [NSIndexPath indexPathForRow:2 inSection:0];
    [self.tableView nicelyMoveRowAtIndexPath:from toIndexPath:to];
}

@end
Community
  • 1
  • 1
Jonathan
  • 6,572
  • 1
  • 30
  • 46
  • This is not working on iOS 8. Still can't find solution. – Slavcho Sep 24 '14 at 09:36
  • @SlavcoPetkovski It still works fine on iOS 8 in the app I built this fix for. Are you sure you've implemented it correctly? – Jonathan Sep 27 '14 at 21:35
  • Yes, it is working now, since I was using fetchedResultsController and he was hiding the checkmarks after refreshing him. So I'm not refreshing now and it is working fine. By refreshing I mean reloading the sections. – Slavcho Sep 29 '14 at 07:34
0

Please try forcing inset size or setting them to zero in the viewDidLoad to ensure tableView respects them.

This is going to set your tableView separator insets to 30.

- (void)viewDidLoad {
    [super viewDidLoad];
    if ([self.tableView respondsToSelector:@selector(setSeparatorInset:)]) {
        [self.tableView setSeparatorInset:UIEdgeInsetsMake(0, 30, 0, 0)];
    }
}

You can also set separator insets only on specific cells as:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCellId"];

    if (indexPath.section == 0 && indexPath.row == 0) {
       [cell setSeparatorInset:UIEdgeInsetsMake(0, 30, 0, 0)];
    } else {
       [cell setSeparatorInset:UIEdgeInsetsZero];
    }
    return cell;
}
Yas Tabasam
  • 10,517
  • 9
  • 48
  • 53
  • I do not want the inset on inner cells to be zero, I want it to be as it is and to appear properly after moving. – Majster Feb 13 '14 at 21:14
  • I changed my answer, you can set them to anything you want. Just change 30 to your desired number. – Yas Tabasam Feb 13 '14 at 21:46
  • Well this makes all insets the same. I want the first and last one without insets and others inset, just the way iOS makes it by default. I want is for the table to stay the way it was before moving (separators of course). – Majster Feb 13 '14 at 21:57
  • Well, in that case you can set them on individual cells then `[cell setSeparatorInset:UIEdgeInsetsMake(0, 50, 0, 0)];` I've updated my answers to reflect this. – Yas Tabasam Feb 13 '14 at 22:22