(Heureux d'accepter une réponse en Swift ou Objective-C)
Ma vue tableau comporte quelques sections et lorsqu'un bouton est enfoncé, je souhaite insérer une ligne à la fin de la section 0. En appuyant à nouveau sur le bouton, je souhaite supprimer cette même ligne. Mon code presque fonctionnel ressemble à ceci:
// 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];
}
Cela se comporte correctement parfois, mais parfois pas, en fonction du défilement de la vue tableau:
Je peux voir cela se produire au ralenti dans le simulateur avec "Debug-> Toggle Slow Animations". Le même problème se produit en sens inverse lors de la suppression.
J'ai constaté que la taille du saut en décalage est liée à la distance parcourue par le tableau dans la section 0: le saut minuscule lorsque le décalage est minime. Le saut devient plus grand à mesure que le défilement approche la moitié de la hauteur totale de la section 0 (le problème est pire ici, saut == la moitié de la hauteur de la section). En faisant défiler plus loin, le saut devient plus petit. Lorsque le tableau défile de sorte que seule une infime partie de la section 0 soit encore visible, le saut est minime.
Pouvez-vous m'aider à comprendre pourquoi et comment résoudre ce problème?
Sur iOS 11, UITableView utilise la hauteur de ligne estimée par défaut.
Cela entraîne des comportements imprévisibles lors de l'insertion, du rechargement ou de la suppression de lignes, car UITableView a une taille de contenu incorrecte, la plupart du temps:
Pour éviter de trop nombreux calculs de mise en page, tableView demande heightForRow
uniquement pour chaque appel cellForRow
et s'en souvient (en mode normal, tableView demande à heightForRow
pour tous les chemins indexPath de la tableView). Le reste des cellules a une hauteur égale à la valeur estimatedRowHeight
jusqu'à ce que leur cellForRow
correspondant soit appelé.
// estimatedRowHeight mode
contentSize.height = numberOfRowsNotYetOnScreen * estimatedRowHeight + numberOfRowsDisplayedAtLeastOnce * heightOfRow
// normal mode
contentSize.height = heightOfRow * numberOfCells
Une solution consiste à désactiver le mode estimatedRowHeight
en définissant la propriété estimationRowHeight sur 0 et en implémentant la propriété heightForRow
pour chacune de vos cellules.
Bien sûr, si vos cellules ont des hauteurs dynamiques (la plupart du temps, avec des calculs de mise en page onéreux, vous avez donc utilisé estimatedRowHeight
pour une bonne raison), vous devez trouver un moyen de reproduire l'optimisation estimatedRowHeight
sans compromettre la taille de contentView de votre table. Jetez un coup d'œil à AsyncDisplayKit ou UITableView-FDTemplateLayoutCell .
Une autre solution consiste à essayer de trouver une variable estimatedRowHeight
qui convienne bien. Sur iOS 11, en particulier, vous pouvez essayer d'utiliser UITableViewAutomaticDimension
pour EstimationRowHeight:
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = UITableViewAutomaticDimension
Je ne sais pas comment le réparer correctement, mais ma solution fonctionne pour moi.
// kostyl: for fix jumping of tableView as for tableView diffucult to calculate height of cells
tableView.kostylAgainstJumping {
if oldIsFolded {
tableView.insertRows(at: indexPaths, with: .fade)
} else {
tableView.deleteRows(at: indexPaths, with: .fade)
}
}
extension UITableView {
func kostylAgainstJumping(_ block: () -> Void) {
self.contentInset.bottom = 300
block()
self.contentInset.bottom = 0
}
}
Cela se produisait pour moi sur un UITableView comportant plusieurs sections, mais aucune définition de ce que sa hauteur d'en-tête ou de vue ne devrait être pour ces sections. L'ajout des méthodes de délégué suivantes a résolu le problème pour moi. J'espère que cela aidera!
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
J'ai corrigé jump en mettant en cache la hauteur des lignes de cellules, ainsi que la hauteur des pieds de section et des en-têtes. L'approche nécessite d'avoir un identifiant de cache unique pour les sections et les lignes.
// 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:
...
}
}
La classe réutilisable pour les caches peut ressembler à celle ci-dessous:
#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