Why does my UITableView “jump” when inserting or removing a row?

后端 未结 10 1172
小蘑菇
小蘑菇 2020-12-05 10:21

(Happy to accept an answer in Swift or Objective-C)

My table view has a few sections, and when a button is pressed, I want to insert a row at the end of section 0.

相关标签:
10条回答
  • 2020-12-05 10:21

    @GaétanZ's solution did not work for me (IOS12) but he's concept is right..

    SO I did the next logical step:

    IF table content does not know how tall is the cell THEN lets just “keep on scrolling" down RIGHT AFTER inserting the cell

    private func insertBottomBubble(withCompletionHandler completion: (() -> Void)?) {
        let bottomIndexPath = IndexPath(row: cbModelViewController!.viewModelsCount - 1, section: 0)
    
    
        CATransaction.begin()
        CATransaction.setAnimationDuration(0.9)
        CATransaction.setCompletionBlock {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                self.scrollToBottom(withCompletionHandler: completion)
            }
        }
        tableView.insertRows(at: [bottomIndexPath], with: isLeft == true ? .left : .right)
        self.scrollToBottom(withCompletionHandler: nil) // no jump, keep it down :D
        CATransaction.commit()
    }
    
    
    func scrollToBottom(withCompletionHandler completion: (() -> Void)?) {
        let bottomMessageIndexPath = IndexPath(row: tableView.numberOfRows(inSection: 0) - 1, section: 0)
        UIView.animate(withDuration: 0.45,
                       delay: TimeInterval(0),
                       options: UIView.AnimationOptions.curveEaseInOut,
                       animations: {
                        self.tableView.scrollToRow(at: bottomMessageIndexPath, at: .bottom, animated: false)
        },
                       completion: { success in
                        if success {
                            completion?()
                        }
    
        })
    

    iOS 12 tested only

    0 讨论(0)
  • 2020-12-05 10:21

    This problem occurs if the height of the cells varies greatly. Vlad’s solution works great. But difficult to implement. I suggest a simpler way. It will help you in most cases.

    Add a variable private var cellHeightCache = [IndexPath: CGFloat]() to your controller. And implement two delegate methods:

    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
       return cellHeightCache[indexPath] ?? 44
    }
    
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
       cellHeightCache[indexPath] = cell.bounds.height
    }
    
    0 讨论(0)
  • 2020-12-05 10:28

    What everyone is saying about estimated row heights is true. So taking all of that into consideration here's the idea:

    Store the heights of each row in a data structure (I choose a dictionary), then use that value from the dictionary for heightForRowAtIndexPath AND estimatedHeightForRowAtIndexPath methods

    So the question is, how to get the row height if you are using dynamic label sizing. Well simple, just use the willDisplayCell method to find the cell frame

    Here is my total working version, and sorry for the objective c...its just the project I'm working on right now:

    declare a property for your dictionary:

    @property (strong) NSMutableDictionary *dictionaryCellHeights;
    

    init the dictionary:

    self.dictionaryCellHeights = [[NSMutableDictionary alloc]init];
    

    capture height:

    -(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
    
      NSNumber *height = [NSNumber numberWithDouble:cell.frame.size.height];
        NSString *rowString = [NSString stringWithFormat:@"%d", indexPath.row];
        [self.dictionaryCellHeights setObject:height forKey:rowString];
    }
    

    use height:

    -(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
        NSNumber *height = [self getRowHeight:indexPath.row];
        if (height == nil){
            return UITableViewAutomaticDimension;
        }
        return height.doubleValue;
    }
    
    -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
        NSNumber *height = [self getRowHeight:indexPath.row];
        if (height == nil){
            return UITableViewAutomaticDimension;
        }
        return height.doubleValue;
    }
    
    -(NSNumber*)getRowHeight: (int)row{
        NSString *rowString = [NSString stringWithFormat:@"%d", row];
        return [self.dictionaryCellHeights objectForKey:rowString];
    }
    

    Then when inserting the rows:

    [self.tableViewTouchActivities performBatchUpdates:^{
                 [self.tableViewTouchActivities insertRowsAtIndexPaths:toInsertIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
            } completion:^(BOOL finished){
                [self.tableViewTouchActivities finishInfiniteScroll];
            }];
    

    *note - I'm using this library for infiniteScrolling https://github.com/pronebird/UIScrollView-InfiniteScroll/blob/master/README.md

    0 讨论(0)
  • 2020-12-05 10:30

    I don't know how to fix it correctly, but my solution works for me

    // hack: for fix jumping of tableView as for tableView difficult to calculate height of cells
        tableView.hackAgainstJumping {
          if oldIsFolded {
            tableView.insertRows(at: indexPaths, with: .fade)
          } else {
            tableView.deleteRows(at: indexPaths, with: .fade)
          }
        }
    
    
    extension UITableView {
      func hackAgainstJumping(_ block: () -> Void) {
          self.contentInset.bottom = 300
          block()
          self.contentInset.bottom = 0
      }
    }
    
    0 讨论(0)
  • 2020-12-05 10:35

    for me work, turn off automatic estimate for tableview rows, section, headers and I use heightForRowAt

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            if indexPath.row % 2 == 1 {
                if arrowsVisible {
                    return 20
                }
                return 5
            }
    }
    
    0 讨论(0)
  • 2020-12-05 10:37

    On iOS 11, UITableView uses estimated row height as default.

    It leads to unpredictable behaviors when inserting/reloading or deleting rows because the UITableView has a wrong content size most of the time :

    To avoid too many layout calculations, the tableView asks heightForRow only for each cellForRow call and remembers it (in normal mode, the tableView asks heightForRow for all the indexPaths of the tableView). The rest of the cells has a height equal to the estimatedRowHeight value until their corresponding cellForRow is called .

    // estimatedRowHeight mode
    contentSize.height = numberOfRowsNotYetOnScreen * estimatedRowHeight + numberOfRowsDisplayedAtLeastOnce * heightOfRow
    
    // normal mode
    contentSize.height = heightOfRow * numberOfCells
    

    One solution is to disable the estimatedRowHeight mode by setting estimatedRowHeight to 0 and implementing heightForRow for each of your cells.

    Of course, if your cells have dynamic heights (with onerous layout calculations most of time so you used estimatedRowHeight for a good reason), you would have to find a way to reproduce the estimatedRowHeight optimization without compromising the contentSize of your tableView. Take a look at AsyncDisplayKit or UITableView-FDTemplateLayoutCell.

    Another solution is to try to find a estimatedRowHeight which suits well. Since iOS 10, you can also try to use UITableView.automaticDimension. UIKit will find a value for you:

    tableView.rowHeight = UITableView.automaticDimension
    tableView.estimatedRowHeight = UITableView.automaticDimension
    

    On iOS 11, it's already the default value.

    0 讨论(0)
提交回复
热议问题