UIRefreshControl - beginRefreshing ne fonctionne pas lorsque UITableViewController est à l'intérieur de UINavigationController

120

J'ai configuré un UIRefreshControl dans mon UITableViewController (qui est à l'intérieur d'un UINavigationController) et cela fonctionne comme prévu (c'est-à-dire que le pull down déclenche l'événement correct). Cependant, si j'appelle par programme la beginRefreshingméthode d'instance sur le contrôle d'actualisation comme:

[self.refreshControl beginRefreshing];

Rien ne se passe. Il doit s'animer et afficher le spinner. La endRefreshingméthode fonctionne correctement lorsque j'appelle cela après l'actualisation.

J'ai créé un projet de prototype de base avec ce comportement et cela fonctionne correctement lorsque mon UITableViewController est ajouté directement au contrôleur de vue racine du délégué d'application, par exemple:

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

Mais si j'ajoute d'abord le tableViewControllerà un UINavigationController, puis ajoute le contrôleur de navigation en tant que rootViewController, la beginRefreshingméthode ne fonctionne plus. Par exemple

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

Mon sentiment est que cela a quelque chose à voir avec les hiérarchies de vues imbriquées dans le contrôleur de navigation qui ne jouent pas bien avec le contrôle de rafraîchissement - des suggestions?

Merci

wow
la source

Réponses:

195

Il semble que si vous commencez à actualiser par programme, vous devez faire défiler la vue du tableau vous-même, par exemple, en modifiant contentoffset

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

Je suppose que la raison à cela est qu'il pourrait être indésirable de faire défiler jusqu'au contrôle de rafraîchissement lorsque l'utilisateur est au milieu / en bas de la vue de la table?

Version Swift 2.2 par @muhasturk

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

En un mot, pour garder ce portable, ajoutez cette extension

UIRefreshControl + ProgramatelyBeginRefresh.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
la source
6
C'est étrange, dans mes tests, endRefreshing ajuste le décalage au besoin
Dmitry Shevchenko
9
BTW, si vous utilisez la mise en page automatique, vous pouvez remplacer la ligne dans la réponse par ceci: [self.tableView setContentOffset: CGPointMake (0, -self.topLayoutGuide.length) animé: OUI];
Eric Baker
4
@EricBaker Je pense que cela ne fonctionnera pas. Tous les UITableViewControllers n'ont pas de barres de navigation. Cela conduirait à un topLayoutGuide de longueur 20 et à un décalage trop petit.
Fábio Oliveira
3
@EricBaker vous pouvez utiliser: [self.tableView setContentOffset: CGPointMake (0, self.topLayoutGuide.length -self.refreshControl.frame.size.height) animé: OUI];
ZYiOS
1
Le copier-coller fonctionne pour moi, ce qui est génial. J'étais fatigué des trucs de constructeur de storyboard d'Apple.
okysabeni
78

UITableViewController a la propriété automaticAdjustsScrollViewInsets après iOS 7. La vue de table peut déjà avoir contentOffset, généralement (0, -64).

Ainsi, la bonne façon d'afficher refreshControl après le début de l'actualisation par programmation consiste à ajouter la hauteur de refreshControl à contentOffset existant.

 [self.refreshControl beginRefreshing];
 [self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl.frame.size.height) animated:YES];
J? ai compris
la source
salut, Merci beaucoup, je suis également curieux de connaître le point magique (0, -64) que j'ai rencontré lors du débogage.
inix
2
@inix 20 pour la hauteur de la barre d'état + 44 pour la hauteur de la barre de navigation
Igotit
1
Au lieu de -64vaut mieux utiliser-self.topLayoutGuide.length
Diogo T
33

Voici une extension Swift utilisant les stratégies décrites ci-dessus.

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
la source
Fonctionne pour UITableView.
Juan Boero
10
Je recommanderais de mettre sendActionsForControlEvents(UIControlEvents.ValueChanged)à la fin de cette fonction, sinon la logique logique d'actualisation réelle ne sera pas exécutée.
Colin Basnett le
C'est de loin la manière la plus élégante de le faire. Y compris le commentaire de Colin Basnett pour une meilleure fonctionnalité. il peut être utilisé dans tout le projet en le définissant une fois!
JoeGalind
1
@ColinBasnett: L'ajout de ce code (qui est maintenant sendActions (pour: UIControlEvents.valueChanged)), entraîne une boucle infinie ...
Kendall Helmstetter Gelner
15

Aucune des autres réponses n'a fonctionné pour moi. Ils feraient apparaître et tourner le spinner, mais l'action de rafraîchissement elle-même ne se produirait jamais. Cela marche:

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];

Notez que cet exemple utilise une vue de table, mais il aurait tout aussi bien pu être une vue de collection.

Kyle Robson
la source
Cela a résolu mon problème. Mais maintenant j'utilise SVPullToRefresh, comment le retirer par programme?
Gank
1
@Gank Je n'ai jamais utilisé SVPullToRefresh. Avez-vous essayé de lire leurs documents? D'après la documentation, il semble assez évident qu'il peut être retiré par programme: "Si vous souhaitez déclencher l'actualisation par programme (par exemple dans viewDidAppear :), vous pouvez le faire avec: [tableView triggerPullToRefresh];" Voir: github.com/samvermette/ SVPullToRefresh
Kyle Robson
1
Parfait! Cela me scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height)
paraissait
13

L'approche déjà mentionnée:

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

rendrait le spinner visible. Mais cela n'animerait pas. La seule chose que j'ai changé est l'ordre de ces deux méthodes et tout a fonctionné:

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

Pour Swift 4 / 4.1

Un mélange de réponses existantes fait le travail pour moi:

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

J'espère que cela t'aides!

Isaac Bosca
la source
8

Voici l' extension Swift 3 et ultérieure qui montre le spinner et l'anime.

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
la source
2
De toutes les réponses ci-dessus, c'est la seule que j'ai pu obtenir sur iOS 11.1 / xcode 9.1
Bassebus
1
C'est en asyncAfterfait ce qui fait fonctionner l'animation spinner (iOS 12.3 / Xcode 10.2)
francybiga
7

Voir aussi cette question

UIRefreshControl n'affiche pas de spiny lors de l'appel de beginRefreshing et contentOffset est 0

Cela ressemble à un bogue pour moi, car cela ne se produit que lorsque la propriété contentOffset de la tableView est à 0

J'ai corrigé cela avec le code suivant (méthode pour 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
la source
1
Ce n'est pas vrai, j'ai deux UIViewControllers différents qui ont tous deux un contentOffset de 0 lors de viewDidLoad et l'un d'eux tire correctement le refreshControl lors de l'appel [self.refreshControl beginRefreshing] et l'autre pas: /
simonthumper
La documentation ne dit rien sur l'affichage du contrôle beginRefreshing, seulement que son état change. Comme je le vois, il s'agit d'empêcher de lancer l'action d'actualisation deux fois, de sorte qu'une actualisation appelée par programme serait toujours en cours d'exécution, une action initiée par l'utilisateur n'en démarrera pas une autre.
Koen.
J'ai noté des problèmes avec l'utilisation de la méthode setContentOffset: animated, donc cette solution a fonctionné pour moi.
Marc Etcheverry
4

Cela fonctionne parfaitement pour moi:

Swift 3:

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

Pour Swift 5, pour moi, la seule chose qui manquait était d'appeler refreshControl.sendActions(.valueChanged). J'ai fait une extension pour le rendre plus propre.

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
la source
3

En plus de la solution @Dymitry Shevchenko.

J'ai trouvé une bonne solution de contournement à ce problème. Vous pouvez créer une extension pour UIRefreshControlcette méthode de remplacement:

// 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];
    }
}

Vous pouvez utiliser une nouvelle classe en définissant une classe personnalisée dans l' Inspecteur d'identité pour le contrôle d'actualisation dans Interface Builder.

Lyzkov
la source
3

Fort Swift 2.2+

    self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.frame.size.height), animated: true)
Muhasturk
la source
J'ai fusionné cela avec la réponse acceptée car ce n'est qu'une mise à jour.
Tudor
3

Si vous utilisez Rxswift pour swift 3.1, vous pouvez utiliser ci-dessous:

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)
    }
}

Cela fonctionne pour swift 3.1, iOS 10.

jkyin
la source
1
C'est le sendActionsdéclencheur rxqui rend cette réponse liée au RxSwiftcas où tout le monde se demande à première vue
carbonr
J'ai dû utiliser l'instance refreshControl de tableViewController, pas de tableView. De plus, ce setContentOffset ne fonctionnait pas pour moi ciblant iOS10. Celui-ci fonctionne cependant: self.tableView.setContentOffset(CGPoint(x:0, y:self.tableView.contentOffset.y - (refreshControl.frame.size.height)), animated: true)
nmdias
1

testé sur Swift 5

utiliser ceci dans 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
la source
0

J'utilise la même technique pour afficher le signe visuel «les données sont mises à jour». Un utilisateur de résultat apportera une application à partir de l'arrière-plan et les flux / listes seront mis à jour avec l'interface utilisateur, comme les utilisateurs tirent des tables pour se rafraîchir. Ma version contient 3 choses

1) Qui envoie "réveiller"

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

2) Observateur dans UIViewController

- (void)viewDidLoad {
    [super viewDidLoad];

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

3) Le protocole

#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];
    }
}

résultat

WINSergey
la source