ios - uitableviewcell - reloadData() de UITableView con alturas de celda dinámicas provoca un desplazamiento irregular
uitableviewautomaticdimension (17)
Aquí hay una versión un poco más corta:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.cellHeightsDictionary[indexPath] ?? UITableViewAutomaticDimension
}
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, a medida que 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.
De hecho, puede recargar solo ciertas filas utilizando
reloadRowsAtIndexPaths
, por ejemplo:
func reloadSectionWithouAnimation(section: Int) {
UIView.performWithoutAnimation {
let offset = self.contentOffset
self.reloadSections(IndexSet(integer: section), with: .none)
self.contentOffset = offset
}
}
Pero, en general, también podría animar los cambios de altura de celda de la tabla de esta manera:
tableView.reloadSectionWithouAnimation(section: indexPath.section)
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 que
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
}
Este me funcionó en Swift4:
extension UITableView {
func reloadWithoutAnimation() {
let lastScrollOffset = contentOffset
reloadData()
layoutIfNeeded()
setContentOffset(lastScrollOffset, animated: false)
}
}
Hay un error que creo que se introdujo en iOS11.
Es entonces cuando vuelve a
reload
el tableView
contentOffSet
se altera inesperadamente.
De hecho,
contentOffset
no debería cambiar después de una recarga.
Tiende a suceder debido a errores de cálculo de
UITableViewAutomaticDimension
contentOffSet
guardar su
contentOffSet
y volver a establecerlo en su valor guardado una vez que finalice la recarga.
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 deriva de here
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 hacer 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()
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
y hacer
reloadData()
normal
reloadData()
Intente llamar a
cell.layoutSubviews()
antes de devolver la celda en
func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?
.
Es un error conocido en iOS8.
Me encontré con esto hoy y observé:
- Es solo iOS 8, de hecho.
-
Anular
cellForRowAtIndexPath
no ayuda.
La solución fue en realidad bastante simple:
Reemplace
estimatedHeightForRowAtIndexPath
y asegúrese de que devuelva los valores correctos.
Con esto, todos los temblores y saltos extraños en mis UITableViews se han detenido.
NOTA: De hecho, sé el tamaño de mis celdas.
Solo hay dos valores posibles.
Si sus celdas son verdaderamente de tamaño variable, entonces es posible que desee almacenar en caché
cell.bounds.size.height
de
tableView:willDisplayCell:forRowAtIndexPath:
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 ()
Para evitar saltos, debe guardar las alturas de las celdas cuando se cargan y dar el valor exacto en
tableView:estimatedHeightForRowAtIndexPath
:
// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary;
// initialize in code (thanks to @Gerharbo)
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;
}
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
Reemplazar el método estimadoHeightForRowAtIndexPath con un valor alto, por ejemplo 300f
Esto debería solucionar el problema :)
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)
}
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 tabla estaba dentro de una vista de pila, y a veces llamamos
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".
Utilizo más formas de solucionarlo:
Para el controlador de vista:
tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)
como la extensión para UITableView
tableView.beginUpdates()
tableView.endUpdates()
El resultado es
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
}
Versión Swift 3 de la 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
}
La
respuesta de
@Igor
funciona bien en este caso, el código
Swift-4
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 UITableViewAutomaticDimension
}