ios - tutorial - ¿Recibe una notificación cuando UITableView ha terminado de solicitar datos?
uitableview tutorial (18)
¿No se llama a UITableView
layoutSubviews
justo antes de que la vista de tabla muestre su contenido? Me di cuenta de que se llama una vez que la vista de tabla ha terminado de cargar sus datos, tal vez deberías investigar en esa dirección.
¿Hay alguna forma de averiguar cuándo una UITableView
ha terminado de solicitar datos de su fuente de datos?
Ninguno de los viewDidLoad
/ viewWillAppear
/ viewDidAppear
del controlador de vista asociado ( UITableViewController
) son útiles aquí, ya que todos se UITableViewController
demasiado pronto. Ninguno de ellos (comprensiblemente) garantiza que las consultas a la fuente de datos hayan finalizado por el momento (p. Ej., Hasta que la vista se dispare).
Una solución que he encontrado es llamar a reloadData
en viewDidAppear
, ya que, cuando reloadData
regresa, se garantiza que la vista de tabla ha terminado de consultar la fuente de datos todo lo que necesita por el momento.
Sin embargo, esto parece bastante desagradable, ya que supongo que está causando que la fuente de datos se reloadData
la misma información dos veces (una automáticamente y una vez debido a la llamada reloadData
) cuando se carga por primera vez.
La razón por la que quiero hacer esto es porque quiero preservar la posición de desplazamiento de UITableView
, pero hasta el nivel de píxel, no solo a la fila más cercana.
Al restaurar la posición de desplazamiento (usando scrollRectToVisible:animated:
, necesito que la vista de tabla ya tenga suficientes datos en ella, o la llamada de método scrollRectToVisible:animated:
no hace nada (que es lo que sucede si realiza la llamada por sí mismo) en cualquiera de viewDidLoad
, viewWillAppear
o viewDidAppear
).
¿Por qué no solo extender?
@interface UITableView(reloadComplete)
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;
@end
@implementation UITableView(reloadComplete)
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
[self reloadData];
if(completionBlock) {
completionBlock();
}
}
@end
desplazarse hasta el final:
[self.table reloadDataWithCompletion:^{
NSInteger numberOfRows = [self.table numberOfRowsInSection:0];
if (numberOfRows > 0)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:numberOfRows-1 inSection:0];
[self.table scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
}];
No probado con una gran cantidad de datos
Aquí hay una posible solución, aunque es un truco:
[self.tableView reloadData];
[self performSelector:@selector(scrollTableView) withObject:nil afterDelay:0.3];
Donde su método -scrollTableView
desplaza la vista de tabla con -scrollRectToVisible:animated:
Y, por supuesto, puede configurar la demora en el código anterior de 0.3 a lo que parece funcionar para usted. Sí, es ridículamente hacky, pero funciona para mí en mi iPhone 5 y 4S ...
Desde iOS 6 en adelante, el método delegado UITableview
llamado:
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
se ejecutará una vez que su tabla se vuelva a cargar correctamente. Puede hacer la personalización según lo requerido en este método.
Encontré algo similar para recibir notificaciones de cambios en TableView
de TableView
. Creo que debería funcionar aquí también, ya que contentSize también cambia con la carga de datos.
Prueba esto:
En viewDidLoad
escribe,
[self.tableView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:NULL];
y agrega este método a tu viewController:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"contentSize"]) {
DLog(@"change = %@", change.description)
NSValue *new = [change valueForKey:@"new"];
NSValue *old = [change valueForKey:@"old"];
if (new && old) {
if (![old isEqualToValue:new]) {
// do your stuff
}
}
}
}
Es posible que necesite pequeñas modificaciones en el cheque de cambio. Aunque esto había funcionado para mí.
¡Aclamaciones! :)
Esa es mi solución. 100% funciona y se usa en muchos proyectos. Es una subclase sencilla de UITableView.
@protocol MyTableViewDelegate<NSObject, UITableViewDelegate>
@optional
- (void)tableViewWillReloadData:(UITableView *)tableView;
- (void)tableViewDidReloadData:(UITableView *)tableView;
@end
@interface MyTableView : UITableView {
struct {
unsigned int delegateWillReloadData:1;
unsigned int delegateDidReloadData:1;
unsigned int reloading:1;
} _flags;
}
@end
@implementation MyTableView
- (id<MyTableViewDelegate>)delegate {
return (id<MyTableViewDelegate>)[super delegate];
}
- (void)setDelegate:(id<MyTableViewDelegate>)delegate {
[super setDelegate:delegate];
_flags.delegateWillReloadData = [delegate respondsToSelector:@selector(tableViewWillReloadData:)];
_flags.delegateDidReloadData = [delegate respondsToSelector:@selector(tableViewDidReloadData:)];
}
- (void)reloadData {
[super reloadData];
if (_flags.reloading == NO) {
_flags.reloading = YES;
if (_flags.delegateWillReloadData) {
[(id<MyTableViewDelegate>)self.delegate tableViewWillReloadData:self];
}
[self performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f];
}
}
- (void)finishReload {
_flags.reloading = NO;
if (_flags.delegateDidReloadData) {
[(id<MyTableViewDelegate>)self.delegate tableViewDidReloadData:self];
}
}
@end
Es similar a la solución de Josh Brown con una excepción. No se necesita demora en el método performSelector. No importa cuánto tiempo tome reloadData
. tableViewDidLoadData:
siempre se dispara cuando tableView
termina de solicitar dataSource
cellForRowAtIndexPath
.
Incluso si no desea subclasificar UITableView
, simplemente puede llamar a [performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f]
y su selector se llamará justo después de que la tabla termine de recargarse. Pero debe asegurarse de que el selector se llame solo una vez por llamada para reloadData
:
[self.tableView reloadData];
[self performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f];
Disfrutar. :)
Esta es una respuesta a una pregunta ligeramente diferente: necesitaba saber cuándo UITableView
también había terminado de llamar a cellForRowAtIndexPath()
. layoutSubviews()
(gracias @Eric MORAND) y agregué una devolución de llamada de delegado:
SDTableView.h:
@protocol SDTableViewDelegate <NSObject, UITableViewDelegate>
@required
- (void)willReloadData;
- (void)didReloadData;
- (void)willLayoutSubviews;
- (void)didLayoutSubviews;
@end
@interface SDTableView : UITableView
@property(nonatomic,assign) id <SDTableViewDelegate> delegate;
@end;
SDTableView.m:
#import "SDTableView.h"
@implementation SDTableView
@dynamic delegate;
- (void) reloadData {
[self.delegate willReloadData];
[super reloadData];
[self.delegate didReloadData];
}
- (void) layoutSubviews {
[self.delegate willLayoutSubviews];
[super layoutSubviews];
[self.delegate didLayoutSubviews];
}
@end
Uso:
MyTableViewController.h:
#import "SDTableView.h"
@interface MyTableViewController : UITableViewController <SDTableViewDelegate>
@property (nonatomic) BOOL reloadInProgress;
MyTableViewController.m:
#import "MyTableViewController.h"
@implementation MyTableViewController
@synthesize reloadInProgress;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if ( ! reloadInProgress) {
NSLog(@"---- numberOfSectionsInTableView(): reloadInProgress=TRUE");
reloadInProgress = TRUE;
}
return 1;
}
- (void)willReloadData {}
- (void)didReloadData {}
- (void)willLayoutSubviews {}
- (void)didLayoutSubviews {
if (reloadInProgress) {
NSLog(@"---- layoutSubviewsEnd(): reloadInProgress=FALSE");
reloadInProgress = FALSE;
}
}
NOTAS: Dado que esta es una subclase de UITableView
que ya tiene una propiedad de delegado que apunta a MyTableViewController
no es necesario agregar otra. El "delegado @dynamic" le dice al compilador que use esta propiedad. (Aquí hay un enlace que describe esto: http://farhadnoorzay.com/2012/01/20/objective-c-how-to-add-delegate-methods-in-a-subclass/ )
La propiedad UITableView
en MyTableViewController
debe cambiar para usar la nueva clase SDTableView
. Esto se hace en el Inspector de Identidad del Constructor de Interfaces. Seleccione UITableView
dentro del UITableViewController
y configure su "Clase personalizada" en SDTableView
.
La mejor solución que he encontrado en Swift
extension UITableView {
func reloadData(completion: ()->()) {
self.reloadData()
dispatch_async(dispatch_get_main_queue()) {
completion()
}
}
}
Parece que desea actualizar el contenido de la celda, pero sin los saltos repentinos que pueden acompañar a las inserciones y eliminaciones de la celda.
Hay varios artículos sobre cómo hacer eso. Este es uno.
Sugiero usar setContentOffset: animado: en lugar de scrollRectToVisible: animado: para la configuración de píxeles perfectos de una vista de desplazamiento.
Puede cambiar el tamaño de su vista de tabla o establecer su tamaño de contenido en este método cuando se carguen todos los datos:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
tableView.frame =CGRectMake(tableView.frame.origin.x, tableView.frame.origin.y, tableView.frame.size.width, tableView.contentSize.height);
}
Puedes probar la siguiente lógica:
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"MyIdentifier"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
if ( [self chkIfLastCellIndexToCreate:tableView :indexPath]){
NSLog(@"Created Last Cell. IndexPath = %@", indexPath);
//[self.activityIndicator hide];
//Do the task for TableView Loading Finished
}
prevIndexPath = indexPath;
return cell;
}
-(BOOL) chkIfLastCellIndexToCreate:(UITableView*)tableView : (NSIndexPath *)indexPath{
BOOL bRetVal = NO;
NSArray *visibleIndices = [tableView indexPathsForVisibleRows];
if (!visibleIndices || ![visibleIndices count])
bRetVal = YES;
NSIndexPath *firstVisibleIP = [visibleIndices objectAtIndex:0];
NSIndexPath *lastVisibleIP = [visibleIndices objectAtIndex:[visibleIndices count]-1];
if ((indexPath.row > prevIndexPath.row) && (indexPath.section >= prevIndexPath.section)){
//Ascending - scrolling up
if ([indexPath isEqual:lastVisibleIP]) {
bRetVal = YES;
//NSLog(@"Last Loading Cell :: %@", indexPath);
}
} else if ((indexPath.row < prevIndexPath.row) && (indexPath.section <= prevIndexPath.section)) {
//Descending - scrolling down
if ([indexPath isEqual:firstVisibleIP]) {
bRetVal = YES;
//NSLog(@"Last Loading Cell :: %@", indexPath);
}
}
return bRetVal;
}
Y antes de llamar a reloadData, establezca prevIndexPath en nil. Me gusta:
prevIndexPath = nil;
[mainTableView reloadData];
Probé con NSLogs, y esta lógica parece estar bien. Puede personalizar / mejorar según sea necesario.
ReloadData solo solicita datos para las celdas visibles. Dice que para recibir una notificación cuando se cargue la parte específica de la tabla, enganche el tableView: willDisplayCell:
.
- (void) reloadDisplayData
{
isLoading = YES;
NSLog(@"Reload display with last index %d", lastIndex);
[_tableView reloadData];
if(lastIndex <= 0){
isLoading = YES;
//Notify completed
}
- (void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row >= lastIndex){
isLoading = NO;
//Notify completed
}
Simplemente ejecuto repetir el temporizador programado e invalidarlo solo cuando contentSize de la tabla es más grande cuando height de TableHeaderView (significa que hay contenido de filas en la tabla). El código en C # (monotouch), pero espero que la idea sea clara:
public override void ReloadTableData()
{
base.ReloadTableData();
// don''t do anything if there is no data
if (ItemsSource != null && ItemsSource.Length > 0)
{
_timer = NSTimer.CreateRepeatingScheduledTimer(TimeSpan.MinValue,
new NSAction(() =>
{
// make sure that table has header view and content size is big enought
if (TableView.TableHeaderView != null &&
TableView.ContentSize.Height >
TableView.TableHeaderView.Frame.Height)
{
TableView.SetContentOffset(
new PointF(0, TableView.TableHeaderView.Frame.Height), false);
_timer.Invalidate();
_timer = null;
}
}));
}
}
Tenía algo similar, creo. Agregué un BOOL como variable de instancia que me dice si el desplazamiento ha sido restaurado y verifica que en -viewWillAppear:
Cuando no se ha restaurado, lo restauro en ese método y configuro el BOOL para indicar que recuperé el desplazamiento.
Es como un truco y probablemente se puede hacer mejor, pero esto funciona para mí en este momento.
Tuve un mismo escenario en mi aplicación y pensé en publicar mi respuesta para ustedes, ya que otras respuestas mencionadas aquí no funcionan para mí para iOS7 y posterior
Finalmente, esto es lo único que funcionó para mí.
[yourTableview reloadData];
dispatch_async(dispatch_get_main_queue(),^{
NSIndexPath *path = [NSIndexPath indexPathForRow:yourRow inSection:yourSection];
//Basically maintain your logic to get the indexpath
[yourTableview scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionTop animated:YES];
});
Swift Update:
yourTableview.reloadData()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let path : NSIndexPath = NSIndexPath(forRow: myRowValue, inSection: mySectionValue)
//Basically maintain your logic to get the indexpath
yourTableview.scrollToRowAtIndexPath(path, atScrollPosition: UITableViewScrollPosition.Top, animated: true)
})
Entonces, ¿cómo funciona esto?
Básicamente, cuando realizas una recarga, el hilo principal se vuelve ocupado, por lo que en ese momento cuando hacemos un envío de hilo asíncrono, el bloque esperará hasta que se termine el hilo principal. Entonces, una vez que la vista de tabla se haya cargado por completo, el hilo principal se terminará y enviará nuestro bloque de métodos
Probado en iOS7 e iOS8 y funciona increíble;)
Actualización para iOS9: Esto funciona bien también iOS9. He creado un proyecto de muestra en github como POC. https://github.com/ipraba/TableReloadingNotifier
Estoy adjuntando la captura de pantalla de mi prueba aquí.
Entorno probado: simulador iOS9 iPhone6 de Xcode7
finalmente hice que mi código funcionara con esto -
[tableView scrollToRowAtIndexPath:scrollToIndex atScrollPosition:UITableViewScrollPositionTop animated:YES];
había pocas cosas que debían ser atendidas:
- llamarlo dentro de "
- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
" - solo asegúrese de que el mensaje "scrollToRowAtIndexPath" se envíe a una instancia relevante de UITableView, que es definitivamente MyTableview en este caso.
- En mi caso, UIView es la vista que contiene la instancia de UITableView
- Además, se solicitará esto para cada carga de celda. Por lo tanto, coloque una lógica dentro de "cellForRowAtIndexPath" para evitar llamar "scrollToRowAtIndexPath" más de una vez.
EDITAR: Esta respuesta en realidad no es una solución. Probablemente parezca funcionar al principio porque la recarga puede suceder bastante rápido, pero de hecho el bloque de finalización no se llama necesariamente después de que los datos hayan terminado de volverse a cargar, porque reloadData no bloquea. Probablemente deberías buscar una mejor solución.
Para ampliar la respuesta de @Eric MORAND, vamos a poner un bloque de finalización. ¿A quién no le gusta un bloque?
@interface DUTableView : UITableView
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;
@end
y...
#import "DUTableView.h"
@implementation DUTableView
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
[super reloadData];
if(completionBlock) {
completionBlock();
}
}
@end
Uso:
[self.tableView reloadDataWithCompletion:^{
//do your stuff here
}];
Esta respuesta ya no parece funcionar, debido a algunos cambios realizados en la implementación de UITableView desde que se escribió la respuesta. Vea este comentario: reciba una notificación cuando UITableView haya terminado de solicitar datos.
He estado jugando con este problema por un par de días y creo que la subclasificación de UITableView
de reloadData
es el mejor enfoque:
- (void)reloadData {
NSLog(@"BEGIN reloadData");
[super reloadData];
NSLog(@"END reloadData");
}
reloadData
no termina antes de que la tabla termine de cargar sus datos. Entonces, cuando se dispara el segundo NSLog
, la vista de tabla realmente ha terminado de pedir datos.
UITableView
para enviar métodos al delegado antes y después de reloadData
. Funciona a las mil maravillas.