web-dev-qa-db-fra.com

Pourquoi mon UITableView "saute-t-il" lors de l'insertion ou de la suppression d'une ligne?

(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:

  • La section 0 tout en haut, contentOffset.y == 0: fonctionne très bien, la ligne est insérée et les éléments situés sous la section 0 s'animent vers le bas.
  • Section 0 invisible, car le tableau défile dessus: fonctionne parfaitement, le contenu visible situé sous la nouvelle ligne s'anime vers le bas, comme si une ligne était insérée au-dessus de celle-ci.
  • MAIS: si la vue de la table défile un peu, de sorte qu'une partie de la section 0 soit visible: cela fonctionne mal. Dans une seule image, tout le contenu de la vue tableau saute vers le haut (le décalage du contenu augmente) Ensuite, avec l'animation, la nouvelle ligne est insérée et le contenu de la vue tableau recule (le décalage du contenu diminue). Tout se termine là où il devrait être, mais le processus a l'air super mal avec ce "saut" image par image au début.

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?

9
user1272965

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
23
GaétanZ

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
  }
}
2
Leonif

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
}
0
Alex Koshy

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

 enter image description here

0
Vlad