为什么我的UITableView"跳跃"插入或删除行时?

时间:2018-01-08 17:15:57

标签: ios uitableview

(很高兴接受Swift或Objective-C的答案)

我的表视图有几个部分,当按下一个按钮时,我想在第0部分的末尾插入一行。再次按下该按钮,我想删除同一行。我几乎工作的代码看起来像这样:

// model is an array of mutable arrays, one for each section

- (void)pressedAddRemove:(id)sender {
    self.adding = !self.adding;  // this is a BOOL property
    self.navigationItem.rightBarButtonItem.title = (self.adding)? @"Remove" : @"Add";

    // if adding, add an object to the end of section 0
    // tell the table view to insert at that index path

    [self.tableView beginUpdates];
    NSMutableArray *sectionArray = self.model[0];
    if (self.adding) {
        NSIndexPath *insertionPath = [NSIndexPath indexPathForRow:sectionArray.count inSection:0];
        [sectionArray addObject:@{}];
        [self.tableView insertRowsAtIndexPaths:@[insertionPath] withRowAnimation:UITableViewRowAnimationAutomatic];

    // if removing, remove the object from the end of section 0
    // tell the table view to remove at that index path

    } else {
        NSIndexPath *removalPath = [NSIndexPath indexPathForRow:sectionArray.count-1 inSection:0];
        [sectionArray removeObject:[sectionArray lastObject]];
        [self.tableView deleteRowsAtIndexPaths:@[removalPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    }
    [self.tableView endUpdates];
}

这有时会正常运行,但有时不会,这取决于滚动表格视图的位置:

  • 最上面的第0部分,contentOffset.y == 0:效果很好,插入了行,第0部分下面的内容向下动画
  • 第0部分不可见,因为表格滚过它:效果很好,新行下方的可见内容向下动画,好像在其上方插入了一行。
  • 但是:如果表视图滚动了一点,那么部分0的部分是可见的:它工作错误。在单个帧中,表视图中的所有内容都会跳转(内容偏移量增加)然后,通过动画,新行将被插入并且表视图内容向下滚动(内容偏移量减少)。一切都应该到达应有的位置,但是这个过程看起来非常糟糕,只需要一个框架就可以了。#34;跳跃"一开始。

我可以看到这种情况发生在慢动作模拟器中,使用" Debug-> Toggle Slow Animations"。删除时反向出现同样的问题。

我发现偏移量的跳跃大小与表格滚动到第0部分的距离有关:当偏移很小时跳跃很小。当滚动接近第0部分总高度的 half 时,跳跃变得更大(问题在于它在这里最差,跳跃= =部分高度的一半)。进一步滚动,跳跃变小。当滚动表格以便只能看到少量的0部分时,跳跃很小。

你能帮助我理解这是为什么以及如何解决吗?

10 个答案:

答案 0 :(得分:30)

在iOS 11上,UITableView使用估计的行高作为默认值。

在插入/重新加载或删除行时会导致不可预测的行为,因为UITableView在大多数情况下的内容大小都是错误的:

为了避免过多的布局计算,tableView仅为每个heightForRow调用询问cellForRow并记住它(在正常模式下,tableView向heightForRow询问tableView的所有indexPath )。其余单元格的高度等于estimatedRowHeight值,直到调用相应的cellForRow为止。

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

// normal mode
contentSize.height = heightOfRow * numberOfCells

一种解决方案是通过将estimatedRowHeight设置为0并为每个单元格实现estimatedRowHeight来禁用heightForRow模式。

当然,如果您的单元格具有动态高度(大部分时间使用繁重的布局计算,因此您有充分理由使用estimatedRowHeight),则必须找到重现estimatedRowHeight优化的方法在不影响tableView的contentSize的情况下。请查看AsyncDisplayKitUITableView-FDTemplateLayoutCell

另一种解决方案是尝试找到适合的estimatedRowHeight。特别是在iOS 11上,您可以尝试将UITableViewAutomaticDimension用于estimatedRowHeight:

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = UITableViewAutomaticDimension

答案 1 :(得分:3)

我不知道如何正确解决它,但是我的解决方案对我有用

generate series

答案 2 :(得分:2)

尝试禁用UIView动画,对我来说它是可行的。

UIView.setAnimationsEnabled(false)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
UIView.setAnimationsEnabled(true)

答案 3 :(得分:0)

对于我来说,这是在具有多个节的UITableView上发生的,但是没有定义这些节的标题高度或视图的定义。添加以下委托方法可以为我解决此问题-希望对您有所帮助!

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return 0
}

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    return nil
}

答案 4 :(得分:0)

@GaétanZ的解决方案对我没有帮助(IOS12),但他的概念是正确的。

所以我做了下一个逻辑步骤:

  

如果表格内容不知道单元格有多高,那么在插入单元格之后就让“继续滚动”即可吧

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经过测试

答案 5 :(得分:0)

我通过缓存单元格行的高度以及部分页脚和页眉的高度来修复跳转。该方法要求对节和行具有唯一的缓存标识符。

// Define caches
private lazy var sectionHeaderHeights = SmartCache<NSNumber>(type: type(of: self))
private lazy var sectionFooterHeights = SmartCache<NSNumber>(type: type(of: self))
private lazy var cellRowHeights = SmartCache<NSNumber>(type: type(of: self))

// Cache section footer height
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
   let section = sections[section]
   switch section {
   case .general:
      let view = HeaderFooterView(...)
      view.sizeToFit(width: tableView.bounds.width)
      sectionFooterHeights.set(cgFloat: view.bounds.height, forKey: section.cacheID)
      return view
   case .something:
      ...
   }
}

// Cache cell height
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
   let section = sections[indexPath.section]
   switch section {
   case .general:
      cellRowHeights.set(cgFloat: cell.bounds.height, forKey: section.cacheID)
   case .phones(let items):
      let item = items[indexPath.row]
      cellRowHeights.set(cgFloat: cell.bounds.height, forKey: section.cacheID + item.cacheID)
   case .something:
      ...
   }
}

// Use cached section footer height
func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat {
   let section = sections[section]
   switch section {
   default:
      return sectionFooterHeights.cgFloat(for: section.cacheID) ?? 44
   case .something:
      ...
   }
}

// Use cached cell height
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
   let section = sections[indexPath.section]
   switch section {
   case .general:
      return cellRowHeights.cgFloat(for: section.cacheID) ?? 80
   case .phones(let items):
      let item = items[indexPath.row]
      return cellRowHeights.cgFloat(for: section.cacheID + item.cacheID) ?? 120
   case .something:
      ...
   }
}

用于缓存的可重用类如下所示:

#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif

public class SmartCache<ObjectType: AnyObject>: NSCache<NSString, AnyObject> {
}

public extension SmartCache {

   public convenience init(name: String) {
      self.init()
      self.name = name
   }

   public convenience init(type: AnyObject.Type) {
      self.init()
      name = String(describing: type)
   }

   public convenience init(limit: Int) {
      self.init()
      totalCostLimit = limit
   }
}

extension SmartCache {

   public func isObjectCached(key: String) -> Bool {
      let value = object(for: key)
      return value != nil
   }

   public func object(for key: String) -> ObjectType? {
      return object(forKey: key as NSString) as? ObjectType
   }

   public func object(for key: String, _ initialiser: () -> ObjectType) -> ObjectType {
      let existingObject = object(forKey: key as NSString) as? ObjectType
      if let existingObject = existingObject {
         return existingObject
      } else {
         let newObject = initialiser()
         setObject(newObject, forKey: key as NSString)
         return newObject
      }
   }

   public func object(for key: String, _ initialiser: () -> ObjectType?) -> ObjectType? {
      let existingObject = object(forKey: key as NSString) as? ObjectType
      if let existingObject = existingObject {
         return existingObject
      } else {
         let newObject = initialiser()
         if let newObjectInstance = newObject {
            setObject(newObjectInstance, forKey: key as NSString)
         }
         return newObject
      }
   }

   public func set(object: ObjectType, forKey key: String) {
      setObject(object, forKey: key as NSString)
   }
}

extension SmartCache where ObjectType: NSData {

   public func data(for key: String, _ initialiser: () -> Data) -> Data {
      let existingObject = object(forKey: key as NSString) as? NSData
      if let existingObject = existingObject {
         return existingObject as Data
      } else {
         let newObject = initialiser()
         setObject(newObject as NSData, forKey: key as NSString)
         return newObject
      }
   }

   public func data(for key: String) -> Data? {
      return object(forKey: key as NSString) as? Data
   }

   public func set(data: Data, forKey key: String) {
      setObject(data as NSData, forKey: key as NSString)
   }
}

extension SmartCache where ObjectType: NSNumber {

   public func float(for key: String, _ initialiser: () -> Float) -> Float {
      let existingObject = object(forKey: key as NSString)
      if let existingObject = existingObject {
         return existingObject.floatValue
      } else {
         let newValue = initialiser()
         let newObject = NSNumber(value: newValue)
         setObject(newObject, forKey: key as NSString)
         return newValue
      }
   }

   public func float(for key: String) -> Float? {
      return object(forKey: key as NSString)?.floatValue
   }

   public func set(float: Float, forKey key: String) {
      setObject(NSNumber(value: float), forKey: key as NSString)
   }

   public func cgFloat(for key: String) -> CGFloat? {
      if let value = float(for: key) {
         return CGFloat(value)
      } else {
         return nil
      }
   }

   public func set(cgFloat: CGFloat, forKey key: String) {
      set(float: Float(cgFloat), forKey: key)
   }
}

#if os(iOS) || os(tvOS) || os(watchOS)
public extension SmartCache where ObjectType: UIImage {

   public func image(for key: String) -> UIImage? {
      return object(forKey: key as NSString) as? UIImage
   }

   public func set(value: UIImage, forKey key: String) {
      if let cost = cost(for: value) {
         setObject(value, forKey: key as NSString, cost: cost)
      } else {
         setObject(value, forKey: key as NSString)
      }
   }

   private func cost(for image: UIImage) -> Int? {
      if let bytesPerRow = image.cgImage?.bytesPerRow, let height = image.cgImage?.height {
         return bytesPerRow * height // Cost in bytes
      }
      return nil
   }

   private func totalCostLimit() -> Int {
      let physicalMemory = ProcessInfo.processInfo.physicalMemory
      let ratio = physicalMemory <= (1024 * 1024 * 512 /* 512 Mb */ ) ? 0.1 : 0.2
      let limit = physicalMemory / UInt64(1 / ratio)
      return limit > UInt64(Int.max) ? Int.max : Int(limit)
   }
}
#endif

enter image description here

答案 6 :(得分:0)

保存估算的行高

    private var cellHeight = [Int:CGFloat]()
    override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        cellHeight[indexPath.row] = cell.frame.self.height
    }
    override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = cellHeight[indexPath.row] {
        return height
    }
    return tableView.estimatedRowHeight

修复滚动原点Y

    let indexPath = IndexPath(row: INDEX, section: 0)
    tableView.beginUpdates()
    tableView.insertRows(at: [indexPath], with: .fade)
    tableView.endUpdates()
    tableView.setContentOffset(tableView.contentOffset, animated: false)

答案 7 :(得分:0)

如果单元格的高度变化很大,就会发生此问题。 Vlad的解决方案效果很好。但是很难实现。我建议一种更简单的方法。在大多数情况下,它将为您提供帮助。

将变量private var cellHeightCache = [IndexPath: CGFloat]()添加到控制器。并实现两个委托方法:

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
}

答案 8 :(得分:0)

每个人都在谈论估计的行高,这是真的。因此,将所有这些考虑在内是这样的想法:

将每行的高度存储在数据结构中(我选择一个字典),然后将字典中的该值用于heightForRowAtIndexPath和EstimateHeightForRowAtIndexPath方法

所以问题是,如果使用动态标签大小,如何获取行高。好简单,只需使用willDisplayCell方法找到单元格框架

这是我的全部工作版本,对于目标c感到抱歉,它只是我现在正在从事的项目:

为字典声明一个属性:

@property (strong) NSMutableDictionary *dictionaryCellHeights;

初始化字典:

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

捕获高度:

-(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];
}

使用高度:

-(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];
}

然后在插入行时:

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

*注意-我正在使用此库进行infiniteScrolling https://github.com/pronebird/UIScrollView-InfiniteScroll/blob/master/README.md

答案 9 :(得分:0)

对于我的工作,请关闭对表视图行,节,标题的自动估计 我用heightForRowAt

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if indexPath.row % 2 == 1 {
            if arrowsVisible {
                return 20
            }
            return 5
        }
}