UIRefreshControl - beginRefreshing no funciona cuando UITableViewController está dentro de UINavigationController

120

He configurado un UIRefreshControl en mi UITableViewController (que está dentro de un UINavigationController) y funciona como se esperaba (es decir, pull down dispara el evento correcto). Sin embargo, si invoco programáticamente el beginRefreshingmétodo de instancia en el control de actualización como:

[self.refreshControl beginRefreshing];

No pasa nada. Debe animarse y mostrar la ruleta. El endRefreshingmétodo funciona correctamente cuando lo llamo después de la actualización.

Desarrollé un proyecto prototipo básico con este comportamiento y funciona correctamente cuando mi UITableViewController se agrega directamente al controlador de vista raíz del delegado de la aplicación, por ejemplo:

self.viewController = tableViewController;
self.window.rootViewController = self.viewController;

Pero si tableViewControllerprimero agrego el a un UINavigationController, luego agrego el controlador de navegación como el rootViewController, el beginRefreshingmétodo ya no funciona. P.ej

UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:tableViewController];
self.viewController = navController;
self.window.rootViewController = self.viewController;

Mi sensación es que esto tiene algo que ver con las jerarquías de vista anidadas dentro del controlador de navegación que no funcionan bien con el control de actualización, ¿alguna sugerencia?

Gracias

sorprende
fuente

Respuestas:

195

Parece que si comienza a actualizar mediante programación, debe desplazarse por la vista de la tabla usted mismo, por ejemplo, cambiando contentoffset

[self.tableView setContentOffset:CGPointMake(0, -self.refreshControl.frame.size.height) animated:YES];

Supongo que la razón de esto es que podría no ser deseable desplazarse hasta el control de actualización cuando el usuario está en el medio / inferior de la vista de tabla.

Versión Swift 2.2 de @muhasturk

self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.frame.size.height), animated: true)

En pocas palabras, para mantener este portátil agregue esta extensión

UIRefreshControl + PrograntlyBeginRefresh.swift

extension UIRefreshControl {
    func programaticallyBeginRefreshing(in tableView: UITableView) {
        beginRefreshing()
        let offsetPoint = CGPoint.init(x: 0, y: -frame.size.height)
        tableView.setContentOffset(offsetPoint, animated: true)        
    }
}
Dmitry Shevchenko
fuente
6
Eso es extraño, en mis pruebas, endRefreshing ajusta la compensación según sea necesario
Dmitry Shevchenko
9
Por cierto, si está usando el diseño automático, puede reemplazar la línea en la respuesta con esto: [self.tableView setContentOffset: CGPointMake (0, -self.topLayoutGuide.length) animado: YES];
Eric Baker
4
@EricBaker Creo que eso no servirá. No todos los UITableViewControllers muestran barras de navegación. Esto conduciría a un topLayoutGuide de longitud 20 y un desplazamiento demasiado pequeño.
Fábio Oliveira
3
@EricBaker puede usar: [self.tableView setContentOffset: CGPointMake (0, self.topLayoutGuide.length -self.refreshControl.frame.size.height) animado: SÍ];
ZYiOS
1
Copiar y pegar funciona para mí, lo cual es increíble. Me estaba cansando de las cosas del generador de guiones gráficos de Apple.
okysabeni
78

UITableViewController tiene la propiedad automáticamenteAdjustsScrollViewInsets después de iOS 7. Es posible que la vista de tabla ya tenga contentOffset, generalmente (0, -64).

Entonces, la forma correcta de mostrar refreshControl después de comenzar a actualizar mediante programación es agregar la altura de refreshControl al contentOffset existente.

 [self.refreshControl beginRefreshing];
 [self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl.frame.size.height) animated:YES];
Lo tengo
fuente
hola, muchas gracias, también tengo curiosidad por el punto mágico (0, -64) que encontré al depurar.
inix
2
@inix 20 para la altura de la barra de estado + 44 para la altura de la barra de navegación
Igotit
1
En lugar de -64es mejor usarlo-self.topLayoutGuide.length
Diogo T
33

Aquí hay una extensión de Swift que utiliza las estrategias descritas anteriormente.

extension UIRefreshControl {
    func beginRefreshingManually() {
        if let scrollView = superview as? UIScrollView {
            scrollView.setContentOffset(CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height), animated: true)
        }
        beginRefreshing()
    }
}
Chris Wagner
fuente
Funciona para UITableView.
Juan Boero
10
Recomendaría poner sendActionsForControlEvents(UIControlEvents.ValueChanged)al final de esta función, de lo contrario no se ejecutará la lógica lógica de actualización real.
Colin Basnett
Esta es, con mucho, la forma más elegante de hacerlo. Incluyendo el comentario de Colin Basnett para una mejor funcionalidad. se puede utilizar en todo el proyecto definiéndolo una vez.
JoeGalind
1
@ColinBasnett: Agregar ese código (que ahora es sendActions (para: UIControlEvents.valueChanged)), da como resultado un bucle infinito ...
Kendall Helmstetter Gelner
15

Ninguna de las otras respuestas funcionó para mí. Harían que la ruleta se mostrara y girara, pero la acción de actualización en sí nunca sucedería. Esto funciona:

id target = self;
SEL selector = @selector(example);
// Assuming at some point prior to triggering the refresh, you call the following line:
[self.refreshControl addTarget:target action:selector forControlEvents:UIControlEventValueChanged];

// This line makes the spinner start spinning
[self.refreshControl beginRefreshing];
// This line makes the spinner visible by pushing the table view/collection view down
[self.tableView setContentOffset:CGPointMake(0, -1.0f * self.refreshControl.frame.size.height) animated:YES];
// This line is what actually triggers the refresh action/selector
[self.refreshControl sendActionsForControlEvents:UIControlEventValueChanged];

Tenga en cuenta que este ejemplo usa una vista de tabla, pero también podría haber sido una vista de colección.

Kyle Robson
fuente
Esto resolvió mi problema. Pero ahora estoy usando SVPullToRefresh, ¿cómo bajarlo programáticamente?
Gank
1
@Gank Nunca he usado SVPullToRefresh. ¿Has intentado leer sus documentos? Según los documentos, parece bastante obvio que se puede desplegar mediante programación: "Si desea activar la actualización mediante programación (por ejemplo, en viewDidAppear :), puede hacerlo con: [tableView triggerPullToRefresh];" Ver: github.com/samvermette/ SVPullToRefresh
Kyle Robson
1
¡Perfecto! Sin embargo, estaba actuando extraño para mí con la animación, así que simplemente la reemplacé conscrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height)
user1366265
13

El enfoque ya mencionado:

[self.refreshControl beginRefreshing];
 [self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl.frame.size.height) animated:YES];

haría visible la ruleta. Pero no se animaría. Lo único que cambié es el orden de estos dos métodos y todo funcionó:

[self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl.frame.size.height) animated:YES];
[self.refreshControl beginRefreshing];
ancajic
fuente
11

Para Swift 4 / 4.1

Una combinación de respuestas existentes hace el trabajo por mí:

refreshControl.beginRefreshing()
tableView.setContentOffset(CGPoint(x: 0, y: tableView.contentOffset.y - (refreshControl.frame.size.height)), animated: true)

¡Espero que esto ayude!

Isaac Bosca
fuente
7

Ver también esta pregunta

UIRefreshControl no se muestra espinoso cuando se llama a beginRefreshing y contentOffset es 0

Me parece un error, porque solo ocurre cuando la propiedad contentOffset de tableView es 0

Lo arreglé con el siguiente código (método para UITableViewController):

- (void)beginRefreshingTableView {

    [self.refreshControl beginRefreshing];

    if (self.tableView.contentOffset.y == 0) {

        [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){

            self.tableView.contentOffset = CGPointMake(0, -self.refreshControl.frame.size.height);

        } completion:^(BOOL finished){

        }];

    }
}
Peter Lapisu
fuente
1
Esto no es cierto, tengo dos UIViewControllers diferentes, los cuales tienen un contentOffset de 0 en viewDidLoad y uno de ellos baja correctamente el refreshControl al llamar a [self.refreshControl beginRefreshing] y el otro no: /
simonthumper
La documentación no dice nada sobre la visualización del control beginRefreshing, solo que su estado cambia. Como lo veo, es para evitar que se inicie la acción de actualización dos veces, de modo que si una actualización llamada programáticamente aún se esté ejecutando, una acción iniciada por el usuario no iniciará otra.
Koen.
He notado problemas con el uso del método setContentOffset: animated, por lo que esta solución funcionó para mí.
Marc Etcheverry
7

Aquí está Swift 3 y una extensión posterior que muestra el spinner y lo anima.

import UIKit
extension UIRefreshControl {

func beginRefreshingWithAnimation() {

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {

        if let scrollView = self.superview as? UIScrollView {
            scrollView.setContentOffset(CGPoint(x: 0, y: scrollView.contentOffset.y - self.frame.height), animated: true)
          }
        self.beginRefreshing()
      }
   }
}
Ghulam Rasool
fuente
2
De todas las respuestas anteriores, esta fue la única que pude conseguir trabajando en iOS 11.1 / xcode 9.1
Bassebus
1
La asyncAfterrealidad es lo que hace que el trabajo de animación spinner (IOS 12.3 / 10.2 Xcode)
francybiga
4

Funciona perfecto para mí:

Swift 3:

self.tableView.setContentOffset(CGPoint(x: 0, y: -self.refreshControl!.frame.size.height - self.topLayoutGuide.length), animated: true)
Meilbn
fuente
4

Para Swift 5, para mí lo único que faltaba era llamar refreshControl.sendActions(.valueChanged). Hice una extensión para hacerlo más limpio.

extension UIRefreshControl {

    func beginRefreshingManually() {
        if let scrollView = superview as? UIScrollView {
            scrollView.setContentOffset(CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height), animated: false)
        }
        beginRefreshing()
        sendActions(for: .valueChanged)
    }

}
LukasHromadnik
fuente
3

Además de la solución @Dymitry Shevchenko.

Encontré una buena solución a este problema. Puede crear una extensión para UIRefreshControlese método de sobrescritura:

// Adds code forgotten by Apple, that changes content offset of parent scroll view (table view).
- (void)beginRefreshing
{
    [super beginRefreshing];

    if ([self.superview isKindOfClass:[UIScrollView class]]) {
        UIScrollView *view = (UIScrollView *)self.superview;
        [view setContentOffset:CGPointMake(0, view.contentOffset.y - self.frame.size.height) animated:YES];
    }
}

Puede usar una nueva clase configurando una clase personalizada en Identity Inspector para el control de actualización en Interface Builder.

Lyzkov
fuente
3

Fort Swift 2.2+

    self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.frame.size.height), animated: true)
muhasturk
fuente
Fusioné esto con la respuesta aceptada, ya que es solo una actualización.
Tudor
3

Si usa Rxswift para swift 3.1, puede usarlo a continuación:

func manualRefresh() {
    if let refreshControl = self.tableView.refreshControl {
        self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.height), animated: true)
        self.tableView.refreshControl?.beginRefreshing()
        self.tableView.refreshControl?.sendActions(for: .valueChanged)
    }
}

Esto funciona para swift 3.1, iOS 10.

jkyin
fuente
1
Es el sendActionsdisparador lo rxque hace que esta respuesta RxSwiftesté relacionada con el caso de que alguien se esté preguntando a primera vista
carbonr
Tuve que usar la instancia refreshControl del tableViewController, no el tableView. Además, ese setContentOffset no funcionó para mí apuntando a iOS10. Este funciona sin embargo: self.tableView.setContentOffset(CGPoint(x:0, y:self.tableView.contentOffset.y - (refreshControl.frame.size.height)), animated: true)
nmdias
1

probado en Swift 5

usar esto en viewDidLoad()

fileprivate func showRefreshLoader() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
        self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.contentOffset.y - (self.refreshControl.frame.size.height)), animated: true)
        self.refreshControl.beginRefreshing() 
    }
}
Pramodya Abeysinghe
fuente
0

Utilizo la misma técnica para mostrar el signo visual "los datos se actualizan" del usuario. Un usuario trae la aplicación de resultados desde el fondo y las fuentes / listas se actualizarán con la interfaz de usuario, como los usuarios extraen tablas para actualizarse. Mi versión contiene 3 cosas

1) Quién envía "despertar"

- (void)applicationDidBecomeActive:(UIApplication *)application {
    [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationHaveToResetAllPages object:nil];
}

2) Observador en UIViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(forceUpdateData) name:kNotificationHaveToWakeUp:nil];
}

3) El protocolo

#pragma mark - ForcedDataUpdateProtocol

- (void)forceUpdateData {
    self.tableView.contentOffset = CGPointZero;

    if (self.refreshControl) {
        [self.refreshControl beginRefreshing];
        [self.tableView setContentOffset:CGPointMake(0, -self.refreshControl.frame.size.height) animated:YES];
        [self.refreshControl performSelector:@selector(endRefreshing) withObject:nil afterDelay:1];
    }
}

resultado

WINSergey
fuente