reloadData () de UITableView con alturas de celda dinámicas provoca un desplazamiento nervioso

142

Siento que esto podría ser un problema común y me preguntaba si había alguna solución común.

Básicamente, mi UITableView tiene alturas de celda dinámicas para cada celda. Si no estoy en la parte superior de UITableView y yo tableView.reloadData(), el desplazamiento hacia arriba se vuelve nervioso.

Creo que esto se debe al hecho de que debido a que volví a cargar datos, mientras avanzo, el UITableView está recalculando la altura de cada celda que entra en visibilidad. ¿Cómo mitigo eso, o cómo solo recargo Datos desde un IndexPath determinado hasta el final de UITableView?

Además, cuando me las arreglo para desplazarme hasta la parte superior, puedo desplazarme hacia abajo y luego hacia arriba, sin problemas sin saltar. Esto es muy probable porque las alturas UITableViewCell ya se calcularon.

David
fuente
Un par de cosas ... (1) Sí, definitivamente puedes recargar ciertas filas usando reloadRowsAtIndexPaths. Pero (2) ¿qué quiere decir con "saltar" y (3) ha establecido una altura de fila estimada? (Solo trato de averiguar si hay una mejor solución que le permita actualizar la tabla dinámicamente).
Lyndsey Scott,
@ LyndseyScott, sí, he establecido una altura de fila estimada. Por saltar, quiero decir que a medida que me desplazo hacia arriba, las filas se desplazan hacia arriba. Creo que esto se debe a que establecí una altura de fila estimada de 128, y luego a medida que me desplazo hacia arriba, todas mis publicaciones anteriores en UITableView son más pequeñas, por lo que reduce la altura y hace que mi mesa salte. Estoy pensando en hacer reloadRowsAtIndexPaths de fila xa la última fila en mi TableView ... pero como estoy insertando nuevas filas, no funcionará, no puedo saber cuál será el final de mi vista de tabla antes de volver a cargar los datos.
David
2
@LyndseyScott todavía no puedo resolver el problema, ¿hay alguna buena solución?
rad
1
¿Alguna vez encontró una solución para este problema? Estoy experimentando exactamente el mismo problema que se ve en tu video.
user3344977
1
Ninguna de las respuestas a continuación funcionó para mí.
Srujan Simha

Respuestas:

221

Para evitar saltos, debe guardar las alturas de las celdas cuando se cargan y dar un valor exacto en tableView:estimatedHeightForRowAtIndexPath:

Rápido:

var cellHeights = [IndexPath: CGFloat]()

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

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

C objetivo:

// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary = @{}.mutableCopy;

// declare table dynamic row height and create correct constraints in cells
tableView.rowHeight = UITableViewAutomaticDimension;

// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    [cellHeightsDictionary setObject:@(cell.frame.size.height) forKey:indexPath];
}

// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
    if (height) return height.doubleValue;
    return UITableViewAutomaticDimension;
}
Igor
fuente
1
Gracias, realmente me salvas el día :) También funciona en objc
Artem Z.
3
No olvides inicializar cellHeightsDictionary: cellHeightsDictionary = [NSMutableDictionary dictionary];
Gerharbo
1
estimatedHeightForRowAtIndexPath:devuelve un valor doble puede causar un *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]error. Para solucionarlo, en su return floorf(height.floatValue);lugar.
liushuaikobe
Hola @lgor, tengo el mismo problema e intento implementar tu solución. El problema que estoy obteniendo es estimadoHeightForRowAtIndexPath se llama antes de willDisplayCell, por lo que la altura de la celda no se calcula cuando se llama a estimadoHeightForRowAtIndexPath. ¿Alguna ayuda?
Madhuri
1
Las alturas efectivas de @Madhuri deben calcularse en "heightForRowAtIndexPath", que se llama para cada celda en la pantalla justo antes de willDisplayCell, que establecerá la altura en el diccionario para su uso posterior en estimadoRowHeight (en la recarga de la tabla).
Donnit
109

Versión Swift 3 de respuesta aceptada.

var cellHeights: [IndexPath : CGFloat] = [:]


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? 70.0 
}
Casey Wagner
fuente
Gracias esto funcionó muy bien! De hecho, pude eliminar mi implementación de func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {, esto maneja todos los cálculos de altura que necesito.
Natalia
Después de luchar durante muchas horas con saltos persistentes, descubrí que olvidé agregar UITableViewDelegatea mi clase. Es necesario cumplir con ese protocolo porque contiene la willDisplayfunción que se muestra arriba . Espero poder salvar a alguien la misma lucha.
MJQZ1347
Gracias por la respuesta rápida. En mi caso, estaba teniendo un comportamiento SUPER extraño de células que se descomponían al recargar cuando la vista de la tabla se desplazaba hacia / cerca de la parte inferior. Usaré esto de ahora en adelante siempre que tenga celdas de tamaño propio.
Trev14
Funciona perfectamente en Swift 4.2
Adam S.
Un salvavidas. Muy útil al intentar agregar más elementos en la fuente de datos. Evita el salto de las celdas recién agregadas al centro de la pantalla.
Philip Borbon
38

El salto se debe a una mala altura estimada. Cuanto más se diferencie el valor estimado de RowHeight de la altura real, más puede saltar la mesa cuando se vuelve a cargar, especialmente cuanto más abajo se haya desplazado. Esto se debe a que el tamaño estimado de la tabla difiere radicalmente de su tamaño real, lo que obliga a la tabla a ajustar su tamaño de contenido y su compensación. Por lo tanto, la altura estimada no debe ser un valor aleatorio sino cercano a lo que crees que va a ser la altura. También he experimentado cuando configuro UITableViewAutomaticDimension si sus celdas son del mismo tipo, entonces

func viewDidLoad() {
     super.viewDidLoad()
     tableView.estimatedRowHeight = 100//close to your cell height
}

si tienes variedad de celdas en diferentes secciones, entonces creo que el mejor lugar es

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
     //return different sizes for different cells if you need to
     return 100
}
Krishna Kishore
fuente
2
gracias, es exactamente por qué mi tableView estaba tan nervioso.
Louis de Decker
1
Una respuesta anterior, pero aún es real a partir de 2018. A diferencia de todas las otras respuestas, esta sugiere establecer una estimación de RowHeigh una vez en viewDidLoad, lo que ayuda cuando las celdas tienen la misma altura o una muy similar. Gracias Por cierto, esimatedRowHeight se puede configurar a través de Interface Builder en Size Inspector> Table View> Estimate.
Vitalii
Siempre que una altura estimada más precisa me ayudó. También tenía un estilo de vista de tabla agrupada de varias secciones, y tuve que implementarlotableView(_:estimatedHeightForHeaderInSection:)
nteissler el
25

La respuesta de @Igor funciona bien en este caso,Swift-4código de la misma.

// declaration & initialization  
var cellHeightsDictionary: [IndexPath: CGFloat] = [:]  

en los siguientes métodos de UITableViewDelegate

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  // print("Cell height: \(cell.frame.size.height)")
  self.cellHeightsDictionary[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
  if let height =  self.cellHeightsDictionary[indexPath] {
    return height
  }
  return UITableView.automaticDimension
}
Kiran Jasvanee
fuente
66
¿Cómo lidiar con la inserción / eliminación de filas utilizando esta solución? TableView salta, ya que los datos del diccionario no son reales.
Alexey Chekanov
1
¡Funciona genial! especialmente en la última celda cuando se recarga la fila.
Ning
19

He intentado todas las soluciones anteriores, pero nada funcionó.

Después de pasar horas y pasar por todas las frustraciones posibles, descubrí una manera de solucionar esto. ¡Esta solución es un salvavidas! ¡Trabajado como un encanto!

Swift 4

let lastContentOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastContentOffset, animated: false)

Lo agregué como una extensión, para que el código se vea más limpio y evitar escribir todas estas líneas cada vez que quiera volver a cargar.

extension UITableView {

    func reloadWithoutAnimation() {
        let lastScrollOffset = contentOffset
        beginUpdates()
        endUpdates()
        layer.removeAllAnimations()
        setContentOffset(lastScrollOffset, animated: false)
    }
}

finalmente ..

tableView.reloadWithoutAnimation()

O bien, podría agregar estas líneas en su UITableViewCell awakeFromNib()método

layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale

y hacer lo normal reloadData()

Srujan Simha
fuente
1
¿Cómo hace esto para recargar? Usted llama a que reloadWithoutAnimation, pero ¿dónde está la reloadparte?
mate
@matt podría llamar tableView.reloadData()primero y luego tableView.reloadWithoutAnimation(), aún funciona.
Srujan Simha
¡Excelente! Ninguno de los anteriores tampoco funcionó para mí. Incluso todas las alturas y alturas estimadas son totalmente iguales. Interesante.
TY Kucuk
1
No trabajes para mi. Está bloqueado en tableView.endUpdates (). ¡Alguien me puede ayudar!
Kakashi
12

Utilizo más formas de solucionarlo:

Para el controlador de vista:

var cellHeights: [IndexPath : CGFloat] = [:]


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? 70.0 
}

como la extensión para UITableView

extension UITableView {
  func reloadSectionWithouAnimation(section: Int) {
      UIView.performWithoutAnimation {
          let offset = self.contentOffset
          self.reloadSections(IndexSet(integer: section), with: .none)
          self.contentOffset = offset
      }
  }
}

El resultado es

tableView.reloadSectionWithouAnimation(section: indexPath.section)
rastislv
fuente
1
La clave para mí fue implementar su extensión UITableView aquí. Muy inteligente. Gracias rastislv
BennyTheNerd
Funciona perfectamente pero solo tiene un inconveniente: pierde la animación al insertar encabezado, pie de página o fila.
Soufian Hossam
¿Dónde se llamaría reloadSectionWithouAnimation? Entonces, por ejemplo, los usuarios pueden publicar una imagen en mi aplicación (como Instagram); Puedo cambiar el tamaño de las imágenes, pero en la mayoría de los casos tengo que desplazar la celda de la tabla fuera de la pantalla para que eso suceda. Quiero que la celda tenga el tamaño correcto una vez que la tabla pase por reloadData.
Luke Irvin,
11

Me encontré con esto hoy y observé:

  1. Es solo iOS 8, de hecho.
  2. Anular cellForRowAtIndexPathno ayuda.

La solución fue en realidad bastante simple:

Anule estimatedHeightForRowAtIndexPathy asegúrese de que devuelva los valores correctos.

Con esto, todos los temblores extraños y saltos en mis UITableViews se han detenido.

NOTA: De hecho, sé el tamaño de mis celdas. Solo hay dos valores posibles. Si las células son realmente de tamaño variable, entonces es posible que desee almacenar en caché el cell.bounds.size.heightdetableView:willDisplayCell:forRowAtIndexPath:

MarcWan
fuente
2
Se corrigió cuando anulaba el método estimadoHeightForRowAtIndexPath con un valor alto, por ejemplo 300f
Flappy
1
@Flappy es interesante cómo funciona la solución proporcionada por usted y es más corta que otras técnicas sugeridas. Considere publicarlo como respuesta.
Rohan Sanap
9

De hecho, puede volver a cargar solo ciertas filas utilizando reloadRowsAtIndexPaths, por ejemplo:

tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)

Pero, en general, también podría animar los cambios de altura de celda de la tabla de esta manera:

tableView.beginUpdates()
tableView.endUpdates()
Lyndsey Scott
fuente
He probado el método beginUpdates / endUpdates, pero eso solo afecta a las filas visibles de mi tabla. Todavía tengo el problema cuando me desplazo hacia arriba.
David
@David Probablemente porque estás usando alturas de fila estimadas.
Lyndsey Scott
¿Debo deshacerme de mis EstimatedRowHeights y reemplazarlo con beginUpdates y endUpdates?
David
@David No estarías "reemplazando" nada, pero realmente depende del comportamiento deseado ... Si quieres usar la altura estimada de las filas y simplemente volver a cargar los índices debajo de la porción visible actual de la tabla, puedes hacerlo como Dije usando reloadRowsAtIndexPaths
Lyndsey Scott
Uno de mis problemas al probar el método reladRowsAtIndexPaths es que estoy implementando un desplazamiento infinito, por lo que cuando estoy recargando Data es porque acabo de agregar 15 filas más al dataSource. Esto significa que los indexPaths para esas filas aún no existen en UITableView
David
3

Aquí hay una versión un poco más corta:

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

Reemplazar el método estimadoHeightForRowAtIndexPath con un valor alto, por ejemplo 300f

Esto debería solucionar el problema :)

Flappy
fuente
2

Hay un error que creo que se introdujo en iOS11.

Es entonces cuando haces un reloadtableView que contentOffSetse altera inesperadamente. De hecho contentOffset, no debería cambiar después de una recarga. Tiende a suceder debido a errores de cálculo deUITableViewAutomaticDimension

Debe guardar su contentOffSety restablecerlo a su valor guardado después de que su recarga haya finalizado.

func reloadTableOnMain(with offset: CGPoint = CGPoint.zero){

    DispatchQueue.main.async { [weak self] () in

        self?.tableView.reloadData()
        self?.tableView.layoutIfNeeded()
        self?.tableView.contentOffset = offset
    }
}

¿Cómo lo usas?

someFunctionThatMakesChangesToYourDatasource()
let offset = tableview.contentOffset
reloadTableOnMain(with: offset)

Esta respuesta se derivó de aquí

Miel
fuente
2

Este me funcionó en Swift4:

extension UITableView {

    func reloadWithoutAnimation() {
        let lastScrollOffset = contentOffset
        reloadData()
        layoutIfNeeded()
        setContentOffset(lastScrollOffset, animated: false)
    }
}
Dmytro Brovkin
fuente
1

Ninguna de estas soluciones funcionó para mí. Esto es lo que hice con Swift 4 y Xcode 10.1 ...

En viewDidLoad (), declare la altura de fila dinámica de la tabla y cree las restricciones correctas en las celdas ...

tableView.rowHeight = UITableView.automaticDimension

También en viewDidLoad (), registre todas sus puntas de celda tableView en tableview de esta manera:

tableView.register(UINib(nibName: "YourTableViewCell", bundle: nil), forCellReuseIdentifier: "YourTableViewCell")
tableView.register(UINib(nibName: "YourSecondTableViewCell", bundle: nil), forCellReuseIdentifier: "YourSecondTableViewCell")
tableView.register(UINib(nibName: "YourThirdTableViewCell", bundle: nil), forCellReuseIdentifier: "YourThirdTableViewCell")

En tableView heightForRowAt, devuelve una altura igual a la altura de cada celda en indexPath.row ...

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {

    if indexPath.row == 0 {
        let cell = Bundle.main.loadNibNamed("YourTableViewCell", owner: self, options: nil)?.first as! YourTableViewCell
        return cell.layer.frame.height
    } else if indexPath.row == 1 {
        let cell = Bundle.main.loadNibNamed("YourSecondTableViewCell", owner: self, options: nil)?.first as! YourSecondTableViewCell
        return cell.layer.frame.height
    } else {
        let cell = Bundle.main.loadNibNamed("YourThirdTableViewCell", owner: self, options: nil)?.first as! YourThirdTableViewCell
        return cell.layer.frame.height
    } 

}

Ahora proporcione una altura de fila estimada para cada celda en tableView EstimatedHeightForRowAt. Sé lo más preciso que puedas ...

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {

    if indexPath.row == 0 {
        return 400 // or whatever YourTableViewCell's height is
    } else if indexPath.row == 1 {
        return 231 // or whatever YourSecondTableViewCell's height is
    } else {
        return 216 // or whatever YourThirdTableViewCell's height is
    } 

}

Eso debería funcionar...

No necesitaba guardar y establecer contentOffset al llamar a tableView.reloadData ()

Michael Colonna
fuente
1

Tengo 2 alturas de celda diferentes.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
        return Helper.makeDeviceSpecificCommonSize(cellHeight)
    }

Después de agregar estimadoHeightForRowAt , no hubo más saltos.

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
    return Helper.makeDeviceSpecificCommonSize(cellHeight)
}
sabiland
fuente
0

Intenta llamar cell.layoutSubviews()antes de devolver el celular func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. Es un error conocido en iOS8.

CrimeZone
fuente
0

Puede usar lo siguiente en ViewDidLoad()

tableView.estimatedRowHeight = 0     // if have just tableViewCells <br/>

// use this if you have tableview Header/footer <br/>
tableView.estimatedSectionFooterHeight = 0 <br/>
tableView.estimatedSectionHeaderHeight = 0
Vid
fuente
0

Tuve este comportamiento de salto e inicialmente pude mitigarlo estableciendo la altura exacta estimada del encabezado (porque solo tenía 1 vista de encabezado posible), sin embargo, los saltos comenzaron a ocurrir dentro de los encabezados específicamente, sin afectar a toda la tabla.

Siguiendo las respuestas aquí, tuve la idea de que estaba relacionado con las animaciones, por lo que descubrí que la vista de la tabla estaba dentro de una vista de pila, y a veces llamábamos stackView.layoutIfNeeded()dentro de un bloque de animación. Mi solución final fue asegurarme de que esta llamada no suceda a menos que sea "realmente" necesaria, porque el diseño "si es necesario" tenía comportamientos visuales en ese contexto, incluso cuando "no era necesario".

Gobe
fuente
0

Tuve el mismo problema. Tuve paginación y recarga de datos sin animación, pero no ayudó al desplazamiento a evitar saltos. Tengo diferentes tamaños de IPhones, el desplazamiento no estaba nervioso en iphone8 pero sí en iphone7 +

Apliqué los siguientes cambios en la función viewDidLoad :

    self.myTableView.estimatedRowHeight = 0.0
    self.myTableView.estimatedSectionFooterHeight = 0
    self.myTableView.estimatedSectionHeaderHeight = 0

Y mi problema resuelto. Espero que te ayude a ti también.

Burcu Kutluay
fuente
0

Uno de los enfoques para resolver este problema que encontré es

CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock {
   UIView.setAnimationsEnabled(true)
}
tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()
ShaileshAher
fuente
-2

En realidad encontré si usas reloadRowscausando un problema de salto. Entonces deberías intentar usarlo reloadSectionsasí:

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}
Miguel
fuente