19

I'm somewhat stuck with this one… any help is very appreciated. I've already spent lots of time debugging this.

I've got UITableView with data source provided by NSFetchedResultsController. In a separate view controller I insert new records to the CoreData using [NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:], save the managed object context and dismiss that controller. Very standard stuff.

The changes in managed object context are then received by NSFetchedResultsController:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
        [self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
        [self.tableView endUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    switch (type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationNone];

            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

            break;

        case NSFetchedResultsChangeUpdate:
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

            break;

        case NSFetchedResultsChangeMove:
            [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationNone];

            break;
    }
}

And this is where the problem appears — it takes too long(about 3-4 seconds on an iPhone 4) to do that. And it seems like the time is spent calculating layout for the cells.

I've stripped everything from the cell(including custom subclass) and left it with just UILabel, but nothing changed. Then I've changed the style of the cell to Basic(or anything except Custom) and the problem disappeared — new cells are added instantaneously.

I've doubled checked and NSFetchedResultsControllerDelegate callbacks are called only once. If I ignore them and do [UITableView reloadSections:withRowAnimation:], nothing changes — it is still very slow.

It seems to me like Auto Layout is disabled for the default cell styles, which makes them very fast. But if that is the case — why does everything loads quickly when I push the UITableViewController?

Here's the call trace for that problem: stack trace

So the question is — what is going on here? Why are cells being rendered so slowly?

UPDATE 1

I've built a very simple demo app that illustrates the problem I'm having. here's the source — https://github.com/antstorm/UITableViewCellPerformanceProblem

Try adding at least a screenful of cells to feel the performance problems.

Also note that adding a row directly ("Insert now!" button) is not causing any slowness.

  • 2
    Just to confirm - you ARE assigning your custom cells a unique identifier and dequeueing them instead of re-creating, correct? – James Jun 05 '13 at 21:18
  • and what auto-layout rules are specified on the cell? – Wain Jun 05 '13 at 22:12
  • Yes, I am assigning a unique identifier and dequeueing cells using `[UITableView dequeueReusableCellWithIdentifier:forIndexPath:]`. – Anthony Dmitriyev Jun 06 '13 at 07:34
  • As for the layout rules, I tried different ones — fixed width/height and centered UILabel, as well as fixed top/left margins, results are the same. – Anthony Dmitriyev Jun 06 '13 at 07:35
  • 4
    I have experienced the exact same performance issues. I'm still investigating the issue but in the mean time I have just abandoned prototype cells and instead I'm using custom subclasses. Programmatically setting the frames of my cell labels and images inside of `initWithStyle` avoids auto layout performance issues but of couse you also lose all the great things that come with auto layout. There are [some](http://pilky.me/view/36) [good](http://floriankugler.com/blog/2013/4/21/auto-layout-performance-on-ios) reads on auto layout performance that may shed some light on what you are seeing. – Matt Morey Jun 07 '13 at 16:03
  • It is a really good point, thank you for the link. I'm already thinking of custom cells without auto layout. Apparently auto layout is not there yet in terms of performance. The only thing I don't understand is why it doesn't take so long to insert a new cell from the same view? – Anthony Dmitriyev Jun 08 '13 at 16:45

5 Answers5

16

It is true that Auto Layout can present a performance hit. However, for most cases it is not really noticeable. There are some edge cases with complicated layouts where getting rid of it will make a meaningful difference, but that's not really the issue here.

I don't have a good explanation why the app is behaving the way it is, but at least I have a solution: Don't do table view updates if the table view is not on screen. This results in this weird behavior.

In order to this, you can e.g. check for self.tableview.window != nil in the delegate methods of the fetched results controller. Then you just need to add a [self.tableview reloadData] to viewWillAppear so that the table view updates its data before coming on screen.

Hope that helps. And please, if somebody has a good explanation for this weird behavior, please let me know :)

Florian Kugler
  • 688
  • 6
  • 10
  • Yes, `[UITableView reloadData]` really solves the problem, but you won't get animation and it needs to reconfigure all the cells on the screen. Using `[UITableView reloadSections:withRowAnimation:]` can reload table data with animation, but is as slow as the original method. – Anthony Dmitriyev Jun 12 '13 at 13:53
  • 1
    This worked wonders, thanks. I opened rdar://15175803 for this issue. – Léo Natan Oct 08 '13 at 15:26
  • 1
    This solved my similar problem too. I was saving a managed object context in `viewWillAppear`. Moving to to `viewDidAppear` solved the performance problem: something makes the layout much slower if the view isn't on screen. – Alex Pretzlav Jan 30 '14 at 21:47
  • 4
    Thanks for the tip! Avoiding using `beginUpdates`/`endUpdates` when the table is offscreen and calling `reloadData` fixed my performance issues as well. This behavior, as I can tell by the stack traces, is caused by UITableView forcing layout when `endUpdates` is called. When it's offscreen (`tableView.window == nil`), firing Auto Layout causes it to create an offscreen layout engine for each layout operation, which is slow. When on the screen though, window's layout engine is used, so when using `reloadData`, layout is performed normally once the table is back on screen. – Vlas Voloshin Mar 04 '14 at 14:31
  • 3
    Avoiding updates when the view is not on screen really helped, thanks! However, I was hesitant to check for `self.tableview.window != nil` in the delegate methods due to the (theoretical?) risk of unbalanced `beginUpdates`/`endUpdates` calls. Instead, I set the `fetchedResultsController`'s delegate to `nil` in `viewWillDisappear` and reconnect it to `self` in `viewDidAppear` (and `reloadData` too of course). That should ensure that no `fetchedResultsController` delegate methods fire when the view is off-screen. Works well so far. Fingers crossed. – nioq Jun 19 '14 at 14:15
2

Ok, I finally got around this problem without sacrificing animation. My solution is to implement UITableViewCell's interface in a separate Nib file with AutoLayout disabled. It takes a little bit longer to load and you need to positions subviews yourself.

Here's the code to make it possible:

- (void)viewDidLoad {
    [super viewDidLoad];

    ...        

    UINib *rowCellNib = [UINib nibWithNibName:@"RowCell" bundle:nil];
    [self.tableView registerNib:rowCellNib forCellReuseIdentifier:@"ROW_CELL"];
}

Of course you'll need a RowCell.nib file with your cell's view.

While there's no solution to the original problem(which clearly seems a bug to me), I'm using this one.

  • I don't understand this solution. Is auto layout disabled, or do you somehow trick the system to gain performance? – Léo Natan Oct 08 '13 at 14:55
  • The trick is to have separate Xib file(with Auto Layout disabled) for each cell. That way overall interface has auto layout enabled, but each cell — doesn't. – Anthony Dmitriyev Oct 12 '13 at 09:20
  • But cells are not loaded from nib by the table every time. The table view dequeues reusable cells from memory, and they are already loaded from the nib. So basically you have disabled auto layout on the cells. How is that a solution to the problem? – Léo Natan Oct 12 '13 at 11:37
  • I would suggest you try running project I've shared on git. But it seems like the problem is with constraints performance, which are recalculated for the whole set of cells after a reload. Not because of the Nib load. – Anthony Dmitriyev Oct 15 '13 at 06:11
  • I have tried your project and seems to run fine on iOS 7.1 and iOS 7.1.1 on iPhone 4, maybe this was a problem of iOS 6 or iOS 7.0? By the way, I think is not necessary to call [self.tableView beginUpdates] and [self.tableView endUpdates] if you already call to didChangeObject method and handle tableview changes in there. – LightMan Apr 28 '14 at 11:17
  • @LightMan, you actually have to call `[self.tableView beginUpdates]` and `[self.tableView endUpdates]`, because there are cases when there's more that one change happening in between those. So these calls help tableView to stack changes and figure out the best way to visually update content. – Anthony Dmitriyev May 30 '14 at 10:19
2

I has the same performence in [table reloadData] in iOS 7. In my case, I solve this problem to replace the cell configure code from [cell layoutIfNeeded] to the follow code:

[cell setNeedsUpdateConstraints];
[cell setNeedsLayout];

and then return the cell. Evevthing seems ok. I hope this can help other people had the same problem.

Jadian
  • 4,144
  • 2
  • 13
  • 10
0

If you still want to use Auto Layout in your table view cells, there are ways to make it performant. It's a multi-step process, but once you do so, you can have the Auto Layout engine determine a cell's vertical size in addition to the layout of everything else in the cell.

You can find more details here: Using Auto Layout in UITableView for dynamic cell layouts & variable row heights which includes some shortcuts you can take if you are on iOS 8.

Community
  • 1
  • 1
Senseful
  • 86,719
  • 67
  • 308
  • 465
0

I had similar issues with using a complex auto layout in a uitableviewcell subclass. My cell layout is depending on data coming from the server, but the number of cell states is limited to six. So every time I receive nil from dequeueReusableCellWithIdentifier (a new cell is created) I set a dynamic/custom reuseIdentifier for the just created cell (self.dynamicReusableIdentifier). The reuseIdentifier getter is overridden in the UITableViewCell subclass (ACellSubclass):

- (NSString*) reuseIdentifier {

    return self.dynamicReusableIdentifier;
}

The generation of the self.dynamicReusableIdentifier string is based on a static method (configureDynamicReuseIdentifier) defined in the cell class (ACellSubclass). tableView:cellForRowAtIndexPath then picks the correct reusable identifier returned from the

cell = [tableView dequeueReusableCellWithIdentifier:[ACellSubclass configureDynamicReuseIdentifier:someData]];

and the reused cell's static subviews (labels, images) are updated without any auto layout changes.

Vladimír Slavík
  • 1,727
  • 1
  • 21
  • 31