(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.
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
}
Try to disable UIView animation, for me it works.
UIView.setAnimationsEnabled(false)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
UIView.setAnimationsEnabled(true)
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)
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