我通过缓存单元格行的高度以及部分页脚和页眉的高度来修复 jump。方法需要为部分和行提供唯一的缓存标识符。
// 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