¿Qué encontrarás en este post?
TogglePull to refresh en UITableView
Pull to Refresh es un tipo de interacción que Loren Brichter patentó en su cliente de Twitter “Tweetie”. Luego Twitter compró Tweetie, y supongo que ha licenciado la patente a Apple, porque iOS 6 incluye una implementación llamada UIRefreshControl. He aquí un ejemplo de su uso:-(void) viewDidLoad { [super viewDidLoad]; UIRefreshControl *refreshControl = [UIRefreshControl new]; [refreshControl addTarget:self action:@selector(refresh:) forControlEvents:UIControlEventValueChanged]; refreshControl.attributedTitle = [[NSMutableAttributedString alloc] initWithString:@"Pulsa para refrescar..."]; self.refreshControl = refreshControl; } - (void)refresh:(UIRefreshControl *)sender { // ... código de refresco [sender endRefreshing]; }
Lo que vemos es una vista sobre la tabla, un spinner (UIActivityIndicatorView), y un icono animado. Cuando el usuario tira de la tabla, la vista de arriba se queda fija durante el refresco gracias a un tableView.contentInset. Hay muchas implementaciones de este patrón. La de Apple utiliza un “droplet” dibujado en Quartz (imagino que unas 4 páginas al menos). Aquí voy a explicar como hacer una sencilla. El código completo está en GitHub. |
PullView
Empiezo creando la vista que vemos al tirar. Crearé unUIView
con NIB para ahorrar código.
Las dimensiones de la figura son de 120×60. Para cambiar las dimensiones de la vista en Xcode 4.5 hay que seleccionar la vista, e ir a Attributes Inspector > Simulated Metrics > Size > Freeform. Podría haberlo escrito en código, pero la clase no habría quedado así de pequeña:
@interface PullView : UIView @property (nonatomic,weak) IBOutlet UIView *topView; @property (nonatomic,weak) IBOutlet UILabel *topLabel; @property (nonatomic,weak) IBOutlet UIImageView *topArrow; @property (nonatomic,weak) IBOutlet UIView *bottomView; @property (nonatomic,weak) IBOutlet UILabel *bottomLabel; @property (nonatomic,weak) IBOutlet UIImageView *bottomArrow; @end #import "PullView.h" @implementation PullView -(void) awakeFromNib { _topLabel.text = NSLocalizedString(@"pull2view.pull.to.refresh", nil); _bottomLabel.text = [NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"pull2view.last.updated", nil), NSLocalizedString(@"pull2view.last.updated.never",nil)]; } @end
PullToRefreshVC
Esta será una subclase deUITableViewController
con el código necesario para implementar la funcionalidad de refresco.
Lo primero que voy a hacer es cargar el NIB con el PullView y desplazar su frame
para colocarlo sobre el tableView
. El objetivo es que al tirar de la tabla, aparezca la vista que creamos antes.
-(void) viewDidLoad { [super viewDidLoad]; [self setupPullToRefresh]; } -(void) setupPullToRefresh { _pullView = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([PullView class]) owner:[PullView new] options:nil] objectAtIndex:0]; _pullView.frame = CGRectOffset(_pullView.frame, 0, -_pullView.frame.size.height); [self.tableView addSubview:_pullView]; }Ahora hay que ejecutar el refresco cuando el usuario tira hasta hacer visible el pullView y luego suelta. Esto es fácil porque
UITableView
subclasifica UIScrollView
, que tiene una variable contentOffset
que nos informa del movimiento de la tabla.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { bool isPullViewVisible = scrollView.contentOffset.y < -_pullView.frame.size.height; if (isPullViewVisible){ [self refresca]; } }El siguiente paso es mantener el pullView visible mientras el refresco se ejecuta. Esto se hace añadiendo y eliminando el
contentInset
, que es un margen entre el panel de la tabla y el panel del UIScrollView
sobre el que descansa.
-(void) refresh { [UIView animateWithDuration:0.3 animations:^{ [self.tableView setContentInset:UIEdgeInsetsMake(_pullView.frame.size.height, 0, 0, 0)]; }]; // ...refresca los datos... [UIView animateWithDuration:0.3 animations:^{ [self.tableView setContentInset:UIEdgeInsetsZero]; }]; }Faltan todas las animaciones. Voy a animar la flecha circular para que se anime mientras el refresco se ejecuta y luego pare. Aquí está el código:
CATransform3D rotationTransform = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1.0); CABasicAnimation* rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform"]; rotationAnimation.toValue = [NSValue valueWithCATransform3D:rotationTransform]; rotationAnimation.duration = 0.15f; rotationAnimation.cumulative = YES; rotationAnimation.repeatCount = HUGE_VALF; [_pullView.topArrow.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"]; // ... refresco ... // elimina la animación y restaura la posición original [_pullView.topArrow.layer removeAllAnimations]; [_pullView.topArrow layer].transform = CATransform3DMakeRotation(0, 0, 0, 1);Ya solo falta actualizar el texto y la flecha de abajo. El método
scrollViewDidScroll:
se llama repetidamente mientras la vista se desplaza. Dentro de él podemos usar la propiedad contentOffset
para calcular el tanto por uno de la vista que está visible:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView { bool isVisible = scrollView.contentOffset.y<0; if (isVisible) { CGFloat visibility = MIN(1.0, scrollView.contentOffset.y/-_pullView.frame.size.height); [self didPullToVisibility:visibility]; } }El valor de
visibility
irá de 0 (oculto) a 1 (completamente visible). Actualizar la vista requiere unas cuantas líneas más:
// Cambia la flecha y el texto a medida que el usuario arrastra hacia abajo. -(void) didPullToVisibility:(CGFloat)visibility { static bool wasFullyVisible = false; bool isFullyVisible = floorf(visibility); bool valueChanged = wasFullyVisible ^ isFullyVisible; if (valueChanged){ // rota la flecha arriba si la vista está completamente visible, o abajo en caso contrario wasFullyVisible = isFullyVisible; [UIView animateWithDuration:0.2 animations:^{ CGFloat angle = (int)wasFullyVisible * M_PI; // el valor es 0 o PI [_pullView.bottomArrow layer].transform = CATransform3DMakeRotation(angle, 0, 0, 1); }]; // actualiza el texto _pullView.topLabel.text = isFullyVisible ? NSLocalizedString(@"pull2view.release.to.refresh", nil) : NSLocalizedString(@"pull2view.pull.to.refresh", nil); } }Y eso es todo. Me he saltado unos cuantos detalles para hacerlo más breve, pero el proyecto completo está en Github comentado en insultante detalle. Te animo a que preguntes si algo no te ha quedado claro.