Desplazamiento desigual después de actualizar UITableViewCell en su lugar con UITableViewAutomaticDimension

78

Estoy creando una aplicación que tiene una vista de noticias en tiempo real para publicaciones enviadas por usuarios. Esta vista tiene una implementación UITableViewpersonalizada UITableViewCell. Dentro de esta celda, tengo otra UITableViewpara mostrar comentarios. La esencia es algo como esto:

Feed TableView
  PostCell
    Comments (TableView)
      CommentCell
  PostCell
    Comments (TableView)
      CommentCell
      CommentCell
      CommentCell
      CommentCell
      CommentCell

La fuente inicial se descargará con 3 comentarios para obtener una vista previa, pero si hay más comentarios, o si el usuario agrega o elimina un comentario, quiero actualizar el contenido PostCelldentro de la vista de la tabla de fuentes agregando o eliminando CommentCellsla tabla de comentarios en el interior. del PostCell. Actualmente estoy usando el siguiente ayudante para lograr eso:

// (PostCell.swift) Handle showing/hiding comments
func animateAddOrDeleteComments(startRow: Int, endRow: Int, operation: CellOperation) {
  let table = self.superview?.superview as UITableView

  // "table" is outer feed table
  // self is the PostCell that is updating it's comments
  // self.comments is UITableView for displaying comments inside of the PostCell
  table.beginUpdates()
  self.comments.beginUpdates()

  // This function handles inserting/removing/reloading a range of comments
  // so we build out an array of index paths for each row that needs updating
  var indexPaths = [NSIndexPath]()
  for var index = startRow; index <= endRow; index++ {
    indexPaths.append(NSIndexPath(forRow: index, inSection: 0))
  }

  switch operation {
  case .INSERT:
    self.comments.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  case .DELETE:
    self.comments.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  case .RELOAD:
    self.comments.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  }

  self.comments.endUpdates()
  table.endUpdates()

  // trigger a call to updateConstraints so that we can update the height constraint 
  // of the comments table to fit all of the comments
  self.setNeedsUpdateConstraints()
}

override func updateConstraints() {
  super.updateConstraints()
  self.commentsHeight.constant = self.comments.sizeThatFits(UILayoutFittingCompressedSize).height
}

Esto logra la actualización muy bien. La publicación se actualiza en su lugar con comentarios agregados o eliminados dentro del PostCellcomo se esperaba. Estoy usando el tamaño automático PostCellsen la mesa de alimentación. La tabla de comentarios se PostCellexpande para mostrar todos los comentarios, pero la animación es un poco desigual y la tabla se desplaza hacia arriba y hacia abajo una docena de píxeles aproximadamente mientras se lleva a cabo la animación de actualización de la celda.

El salto durante el cambio de tamaño es un poco molesto, pero mi problema principal viene después. Ahora, si me desplazo hacia abajo en la fuente, el desplazamiento es suave como antes, pero si me desplazo hacia arriba por encima de la celda que acabo de cambiar de tamaño después de agregar comentarios, la fuente saltará hacia atrás unas cuantas veces antes de llegar a la parte superior de la fuente. Configuré iOS8celdas de tamaño automático para el Feed de esta manera:

// (FeedController.swift)
// tableView is the feed table containing PostCells
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 560

Si elimino el estimatedRowHeight, la tabla simplemente se desplaza hacia la parte superior cada vez que cambia la altura de una celda. Me siento bastante atrapado en esto ahora y, como nuevo desarrollador de iOS, podría usar cualquier consejo que pueda tener.

Bryan Alger
fuente

Respuestas:

120

Aquí está la mejor solución que encontré para resolver este tipo de problema (problema de desplazamiento + reloadRows + iOS 8 UITableViewAutomaticDimension);

Consiste en mantener todas las alturas en un diccionario y actualizarlas (en el diccionario) ya que tableView mostrará la celda.

Luego devolverá la altura guardada en - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPathmétodo.

Deberías implementar algo como esto:

C objetivo

- (void)viewDidLoad {
    [super viewDidLoad];

    self.heightAtIndexPath = [NSMutableDictionary new];
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [self.heightAtIndexPath objectForKey:indexPath];
    if(height) {
        return height.floatValue;
    } else {
        return UITableViewAutomaticDimension;
    }
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = @(cell.frame.size.height);
    [self.heightAtIndexPath setObject:height forKey:indexPath];
}

Swift 3

@IBOutlet var tableView : UITableView?
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}
dosdos
fuente
2
Solución realmente simple y efectiva. ¡Gracias!
o15a3d4l11s2
1
Solo por curiosidad, ¿hay alguna razón por la que no pueda usar el Diccionario Swift en lugar de NSMutableDictionary? Por cierto, esta solución funciona muy bien, ¡gracias!
AppreciateIt
3
@dosdos ¡Dios te bendiga, amigo!
adnako
5
Para mí, todavía no funciona, tengo un imageView autodefinido y basado en la dimensión del ancho y alto para actualizar su restricción de altura. Incluso con las alturas de celda almacenadas en caché, el desplazamiento sigue saltando, especialmente antes de que una nueva celda con una altura diferente a la actual en la pantalla se desplace a la pantalla.
TonyTony
1
¡Fabuloso! ¡Maravilloso! ¡¡¡¡Está funcionando muy bien !!!!!! Muchas gracias! Creo que puedes marcarlo como la solución.
ndominati2
30

Tuvimos el mismo problema. Proviene de una mala estimación de la altura de la celda que hace que el SDK fuerce una altura incorrecta, lo que provocará el salto de las celdas al retroceder. Dependiendo de cómo construyó su celda, la mejor manera de solucionar esto es implementar el UITableViewDelegatemétodo- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath

Siempre que su estimación esté bastante cerca del valor real de la altura de la celda, esto casi cancelará los saltos y las sacudidas. Así es como lo implementamos, obtendrá la lógica:

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // This method will get your cell identifier based on your data
    NSString *cellType = [self reuseIdentifierForIndexPath:indexPath];

    if ([cellType isEqualToString:kFirstCellIdentifier])
        return kFirstCellHeight;
    else if ([cellType isEqualToString:kSecondCellIdentifier])
        return kSecondCellHeight;
    else if ([cellType isEqualToString:kThirdCellIdentifier])
        return kThirdCellHeight;
    else {
        return UITableViewAutomaticDimension;
    }
}

Se agregó soporte para Swift 2

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    // This method will get your cell identifier based on your data
    let cellType = reuseIdentifierForIndexPath(indexPath)

    if cellType == kFirstCellIdentifier 
        return kFirstCellHeight
    else if cellType == kSecondCellIdentifier
        return kSecondCellHeight
    else if cellType == kThirdCellIdentifier
        return kThirdCellHeight
    else
        return UITableViewAutomaticDimension  
}
Gabriel Cartier
fuente
1
Terminé implementando el método heightForRowAtIndexPath y almacenando en caché el resultado para mejorar el rendimiento, ya que es un poco complicado y la alimentación podría ser larga. Cuando un usuario agrega / elimina o carga comentarios en una de las publicaciones en el feed, invalido el cálculo de altura para esa celda para que se recalcule durante el desplazamiento. Las sacudidas se han ido, desearía poder simplificar el código y aprovechar las nuevas funciones de cálculo de altura, pero no pude hacer que funcione lo suficientemente bien con mi TableViewCell
Bryan Alger
2
¿Has probado con el método que describí anteriormente? Así es como se debe hacer con iOS 8, no se debe calcular la altura ya que el framework ya se encarga de ello. Si implementa el método heightForRowAtIndexPath, simplemente anula el comportamiento del SDK.
Gabriel Cartier
7
@BryanAlger es correcto. Las alturas de fila automáticas simplemente no se pueden utilizar para tablas con muchas filas con mucha variación de altura. Para un desplazamiento suave confiable, debe dar resultados correctos en el método heightForRowAtIndexPath, preferiblemente con alturas de fila almacenadas en caché. De lo contrario, obtendrá sacudidas cuando tableView necesite actualizar su contentSize, especialmente en casos como cuando presiona o muestra otro controlador de vista y regresa. Desafortunadamente, las alturas de fila automáticas solo se pueden usar para tableViews simples con algunas filas.
Jamie Hamick
2
En el caso de que implemente heightForRowAtIndexPath, no está utilizando el poder de la dimensión automática de altura de celda. Claro, implementar eso funcionará, pero sus células no son dinámicas.
Gabriel Cartier
1
Para mi caso, ya había implementado la configuración de celda, el cálculo de altura y el almacenamiento en caché de altura heightForRowAtIndexPath, pero todavía tenía un UITableViewdesplazamiento entrecortado . Seguir la respuesta de @GabrielCartier y agregar una lógica más específica según el tipo de celda realmente ayudó y resolvió el problema, ¡gracias!
Sakiboy
23

La respuesta de dosdos funcionó para mí en Swift 2

Declara el ivar

var heightAtIndexPath = NSMutableDictionary()

en func viewDidLoad ()

func viewDidLoad() {
  .... your code
  self.tableView.rowHeight = UITableViewAutomaticDimension
}

Luego agregue los siguientes 2 métodos:

override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
   let height = self.heightAtIndexPath.objectForKey(indexPath)
   if ((height) != nil) {
     return CGFloat(height!.floatValue)
   } else {
    return UITableViewAutomaticDimension
   }
 }

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
  let height = cell.frame.size.height
  self.heightAtIndexPath.setObject(height, forKey: indexPath)
}

SWIFT 3:

var heightAtIndexPath = [IndexPath: CGFloat]()

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return self.heightAtIndexPath[indexPath] ?? UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    self.heightAtIndexPath[indexPath] = cell.frame.size.height
}
Ranknoodle
fuente
1
¡Gracias! Funciona muy bien :)
Michael
Parpadeo impresionante, eliminado en absoluto.
AVEbrahimi
Actualización agregada para SWIFT 3 (no olvide self.tableView.rowHeight = UITableViewAutomaticDimension en viewDidLoad)
MLBDG
Lamentablemente, no me ayudó mucho. El diseño automático es un poco extraño aquí.
nickdnk
1
he editado tu respuesta para usar un diccionario mecanografiado. Solo quería pegar mi propio código Swift 4 como respuesta, pero descubrí que editar el tuyo sería suficiente. espero que no te importe
manmal
3

La solución @dosdos está funcionando bien

pero hay algo que deberías agregar

siguiendo la respuesta de @dosdos

Rápido 3/4

@IBOutlet var tableView : UITableView!
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}

luego usa estas líneas cuando quieras, para mí las uso dentro de textDidChange

  1. primera recarga Tableview
  2. actualizar restricción
  3. finalmente mover a la vista de tabla superior

    tableView.reloadData()
    self.tableView.layoutIfNeeded()
    self.tableView.setContentOffset(CGPoint.zero, animated: true)
    
Albahaca
fuente
2

Yo también estaba enfrentando el mismo problema. Encontré una solución, pero no soluciona completamente el tirón. Pero parece ser mucho mejor en comparación con el desplazamiento entrecortado anterior.

En su UITableViewmétodo de delegado :cellForRowAtIndexPath:, intente utilizar los dos métodos siguientes para actualizar las restricciones antes de devolver la celda. (Lenguaje rápido)

cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

EDITAR: Es posible que también tenga que jugar con el tableView.estimatedRowHeightvalor para obtener un desplazamiento más suave.

Vishal Chandran
fuente
5
No recomendaría usar este método, llamar a métodos de diseño automático en un método como cellForRowAtIndexPath podría afectar en gran medida el rendimiento de TableView.
Gabriel Cartier
1

Siguiendo la respuesta de @dosdos .

También me pareció interesante implementar: tableView(tableView: didEndDisplayingCell: forRowAtIndexPath:

Especialmente para mi código, donde la celda cambia las restricciones dinámicamente mientras la celda ya se muestra en la pantalla. Actualizar el Diccionario de esta manera ayuda la segunda vez que se muestra la celda.

var heightAtIndexPath = [NSIndexPath : NSNumber]()

....

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = UITableViewAutomaticDimension

....

extension TableViewViewController: UITableViewDelegate {

    //MARK: - UITableViewDelegate

    func tableView(tableView: UITableView,
                   estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {

        let height = heightAtIndexPath[indexPath]

        if let height = height {

            return CGFloat(height)
        }
        else {

            return UITableViewAutomaticDimension
        }
    }

    func tableView(tableView: UITableView,
                   willDisplayCell cell: UITableViewCell,
                                   forRowAtIndexPath indexPath: NSIndexPath) {

        let height: NSNumber = CGRectGetHeight(cell.frame)
        heightAtIndexPath[indexPath] = height
    }

    func tableView(tableView: UITableView,
                   didEndDisplayingCell cell: UITableViewCell,
                                        forRowAtIndexPath indexPath: NSIndexPath) {

        let height: NSNumber = CGRectGetHeight(cell.frame)
        heightAtIndexPath[indexPath] = height
    }
}
Gabriel.Massana
fuente