简体   繁体   中英

UITableView rowHeight animation takes too long

I'm having a problem animating a change in rowHeight for my tableview. The animation is taking a long time (about 3 seconds) on an iPad 3 (first gen retina). However, it only takes this long if I'm expanding the rowHeight when the tableview is near the middle-to-bottom of the list or decreasing the rowHeight in the middle of the list. When it's at the top, the animation works fine. Things to note:

  1. I am subclassing UITableView and overriding the setEditing:animated method, which is where I change the rowHeight.
  2. I'm trying not to use tableView:heightForRowAtIndexPath: as my table can have as many rows as the user wants. Some users may even have hundreds of thousands of rows if they imported data.
  3. When the user puts the table in "editing" mode, I want the row heights to grow by a certain amount (currently by 30.0f ), where I display a range of options for each cell (thinks like "delete", "copy", "print", "share", etc).
  4. Because the row heights are changing, I grab the path of the top-most displayed cell before the rowHeight change and then scroll the tableview to the appropriate path after the rowHeight change so the user doesn't loose their place. If I do not scroll at all, the delay is minimally better (perhaps 2.5 seconds instead of 3) and the list ends up in a completely different place than it started out in.
  5. If I do not animated the changes (aka: beginUpdates & endUpdates ) and instead simply call reloadData , the change is instant. Of course, there's no animation then.
  6. I have tried using [self reloadRowsAtIndexPaths:self.indexPathsForVisibleRows withRowAnimation:UITableViewRowAnimationAutomatic] to no effect (it takes forever).
  7. I have tried placing beginUpdates & endUpdates in just about every conceivable placement within the code, all to no effect.
  8. I've tried computing and directly setting contentOffset , but ended up with some weird effects, like tableCells not being refreshed, etc.
  9. I did some time profiles and discovered most of the time was spent in tableView:cellForRowAtIndexPath: . So I logged the number of times tableView:cellForRowAtIndexPath: gets called during the animation:
    1. Without scrollToRowAtIndexPath:atPosition:animated : 59 times .
    2. With scrollToRowAtIndexPath:atPosition:animated : 67 times .
    3. Using reloadData instead of beginUpdates & endUpdates : 8 times .

Here's the code, which is pretty simple, really:

- (void) setEditing:(BOOL)editing animated:(BOOL)animated{
    CGPoint point = CGPointMake(1, _sectionView.superview ? _sectionView.bounds.size.height + 1 + self.bounds.origin.y : 1 + self.bounds.origin.y);
    NSIndexPath *path = [self indexPathForRowAtPoint:point];
    [super setEditing:editing animated:animated];
    CGFloat rowHeight = self.viewType.viewHeight.floatValue + (self.editing ? FlxRecordTableCellEditingHeight : 0);
    self.rowHeight = rowHeight;
    [self beginUpdates];
    [self endUpdates];
//    [self reloadData];
    [self scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionTop animated:NO];
//    [self reloadRowsAtIndexPaths:self.indexPathsForVisibleRows withRowAnimation:UITableViewRowAnimationAutomatic];
}

It appears that when rowHeight is changed, UITableView calculates what new rows should be at the current contentOffset and then attempt to animate to those new rows (including all rows in between), even though I'm telling it to move to the same cells it started out with. I suspect that if I were to increase the change in rowHeight (say, from 30.0f to 45.0f ), the problem would grow worse as UITableView would have to animate through even more rows for the change. What I want I need is for UITableView to first move to the new cells and then animate the change for only those cells. However, I cannot seem to find a way to do this.

UPDATE

Holy {favorite_euphamism}! ... I've been trying to make tableView:cellForRowAtIndexPath more efficient to no avail. So I ran a separate count of how many times tableView:cellForRowAtIndexPath ends up creating new cells (rather than reuse them). During the animation, UITableView isn't just requesting cells 59 - 67 times, it's creating 59-67 new cells by returning nil for dequeueReusableCellWithIdentifier . No wonder it's taking so long.... and it's spiking my memory as well (thank you Xcode 5 for displaying that...). While I've done as much as I can to make my cells efficient, they're still complex views and definitely not designed for that much creation. There's gotta be a way around this...

Any help or idea would be greatly appreciated. Thanks!

So, the solution I came up with is a hack at best, but it is working for the time being. It's possible that Apple has fixed this problem with the upcoming iOS 7.1 update according to Nikolai Ruhe , but I haven't messed with the beta yet so I can't confirm it.

Unfortunately, I was forced to implement tableView:heightForRowAtIndexPath: . I didn't want to, but luckily most people have updated to iOS 7 and I can conditionally use the estimatedRowHeight property (iOS 7 only) on UITableView to mitigate the performance issues of large tables for iOS 7... every one else kind of gets screwed. I'm close to making my app "iOS 7 Only" so it won't be an issue for long (or for very many users).

I added 3 new iVars to my class: CGFloat _rowHeight , CGFloat _imminentRowHeight , and NSIndexPath *_anchorPath . The _rowHeight variable is returned by default. However, if _anchorPath has been set, tableView:heightForRowAtIndexPath: will conditionally return _imminentRowHeight if indexPath.row >= _anchorPath.row . This keeps the contentOffset for the _anchorPath the same during the transition so that UITableView doesn't try to animated through several dozen tableview cells, which was the original problem:

- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    if (_anchorPath){
        return indexPath.row >= _anchorPath.row ? _imminentRowHeight : _rowHeight;
    }
    return _rowHeight;
}

I created a new method to separate out my logic, updateRowHeight . I'd like to again point out this this is a subclass of UITableView , so replace self with your _tableView iVar if you're doing this from a view controller:

- (void) updateRowHeight{
    CGFloat cellHeight = self.viewType.viewHeight.floatValue;
    CGPoint anchorPoint = CGPointMake(1, _sectionView.superview ? _sectionView.bounds.size.height + 1 + self.bounds.origin.y : 1 + self.bounds.origin.y);
    _anchorPath = [self indexPathForRowAtPoint:anchorPoint];
    _imminentRowHeight = cellHeight + (self.editing ? FlxRecordTableCellEditingHeight : 0);
    if (!_anchorPath){
        _rowHeight = _imminentRowHeight;
        if ([self respondsToSelector:@selector(estimatedRowHeight)]){
            self.estimatedRowHeight = _rowHeight;
        }
        return;
    }
    if (_imminentRowHeight == _rowHeight) return;

    [CATransaction begin];
    [CATransaction setCompletionBlock:^{
        _rowHeight = _imminentRowHeight;
        NSIndexPath *anchor = _anchorPath;
        _anchorPath = nil;
        if ([self respondsToSelector:@selector(estimatedRowHeight)]){
            self.estimatedRowHeight = _rowHeight;
        }
        [self reloadData];
        [self scrollToRowAtIndexPath:anchor atScrollPosition:UITableViewScrollPositionTop animated:NO];
    }];
    [self beginUpdates];
    [self endUpdates];
    //We must scroll the anchor Path into the top position so that when we reload the table data positions don't change
    [self scrollToRowAtIndexPath:_anchorPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
    [CATransaction commit];
}

Some things to note:

  1. I escape out of the method early if no update is needed. This occurs if the row height isn't changing or if no anchorPath was found, which happens if there are no rows displayed or the table hasn't loaded yet. UITableView has to do a lot of work here, so it's best to not do the work unless necessary.
  2. I use CATransaction to set the animation context so that I can attach a completion block to it. begin/endUpdates will use the current animation context if one is available, else it will create its own... very convenient that.
  3. I scroll (animated) the anchor path to the top position so that the table view won't "jump" when I reload the table data. Again, after reloading the data I scroll again (without animation) so that the table appears to be exactly the same as before the reload.
  4. I have to reload the table because I've only change the row heights for cells "below" the anchor path. Until I reload, UITableView thinks everything "above" the anchor path has the previous (and incorrect) row height. This is why I set _rowHeight = _imminentRowHeight; and _anchorPath = nil; before I reload the table. I must then scroll to the anchor path (not animated) after the reload because the offset of the anchor path has changed now that all previous rows have the new row height.
  5. There is a very slight "flicker" when the table reloads. For my app at least, only the cell text "flickers" and that's because I'm using custom drawing routines that asynchronously draw the text. So it's possible there won't be any flicker at all for others. For me, the effect is so slight that I'm probably going to ignore it. I'm toying with the idea of displaying an overlay but since we're talking about a whole screen-worth of content, I doubt it'll work well.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM