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

后端 未结 10 1173
小蘑菇
小蘑菇 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:37

    This was happening for me on a UITableView that had multiple sections, but no definitions for what it's header height or view should be for those sections. Adding the following delegate methods fixed it for me - hope it helps!

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 0
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        return nil
    }
    
    0 讨论(0)
  • 2020-12-05 10:40

    Try to disable UIView animation, for me it works.

    UIView.setAnimationsEnabled(false)
    self.tableView.deleteRows(at: [indexPath], with: .automatic)
    UIView.setAnimationsEnabled(true)
    
    0 讨论(0)
  • 2020-12-05 10:45

    Save estimated row heights

        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
    

    Fix scroll origin Y

        let indexPath = IndexPath(row: INDEX, section: 0)
        tableView.beginUpdates()
        tableView.insertRows(at: [indexPath], with: .fade)
        tableView.endUpdates()
        tableView.setContentOffset(tableView.contentOffset, animated: false)
    
    0 讨论(0)
  • 2020-12-05 10:47

    I fixed jump by caching height of cell rows, as well as height of section footers and headers. Approach require to have unique cache identifier for sections and rows.

    // 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:
          ...
       }
    }
    

    Reusable class for caches can look like below:

    #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
    

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