UIRefreshControl - beginRefreshing non funziona quando UITableViewController è all'interno di UINavigationController


120

Ho installato un UIRefreshControl nel mio UITableViewController (che si trova all'interno di un UINavigationController) e funziona come previsto (ovvero il pull down attiva l'evento corretto). Tuttavia, se richiamo a livello di codice il beginRefreshingmetodo di istanza sul controllo di aggiornamento come:

[self.refreshControl beginRefreshing];

Non succede niente. Dovrebbe animarsi verso il basso e mostrare lo spinner. Il endRefreshingmetodo funziona correttamente quando lo chiamo dopo l'aggiornamento.

Ho creato un prototipo di progetto di base con questo comportamento e funziona correttamente quando il mio UITableViewController viene aggiunto direttamente al controller di visualizzazione radice del delegato dell'applicazione, ad esempio:

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

Ma se aggiungo prima il tableViewControllera un UINavigationController, quindi aggiungo il controller di navigazione come il rootViewController, il beginRefreshingmetodo non funziona più. Per esempio

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

La mia sensazione è che questo abbia qualcosa a che fare con le gerarchie di visualizzazione annidate all'interno del controller di navigazione che non funzionano bene con il controllo di aggiornamento - qualche suggerimento?

Grazie

Risposte:


195

Sembra che se si avvia l'aggiornamento a livello di programmazione, è necessario scorrere personalmente la visualizzazione della tabella, ad esempio, modificando l'offset del contenuto

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

Immagino che la ragione di ciò sia che potrebbe essere indesiderabile scorrere fino al controllo di aggiornamento quando l'utente si trova nella parte centrale / inferiore della visualizzazione tabella?

Versione Swift 2.2 di @muhasturk

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

In poche parole, per mantenere questo portatile aggiungi questa estensione

UIRefreshControl + ProgramaticallyBeginRefresh.swift

extension UIRefreshControl {
    func programaticallyBeginRefreshing(in tableView: UITableView) {
        beginRefreshing()
        let offsetPoint = CGPoint.init(x: 0, y: -frame.size.height)
        tableView.setContentOffset(offsetPoint, animated: true)        
    }
}

6
È strano, nei miei test, endRefreshing regola l'offset secondo necessità
Dmitry Shevchenko

9
A proposito, se stai usando il layout automatico, puoi sostituire la riga nella risposta con questa: [self.tableView setContentOffset: CGPointMake (0, -self.topLayoutGuide.length) animated: YES];
Eric Baker

4
@EricBaker credo che non funzionerà. Non tutti gli UITableViewControllers mostrano le barre di navigazione. Ciò porterebbe a una topLayoutGuide di lunghezza 20 e un offset troppo piccolo.
Fábio Oliveira

3
@EricBaker puoi usare: [self.tableView setContentOffset: CGPointMake (0, self.topLayoutGuide.length -self.refreshControl.frame.size.height) animated: YES];
ZYiOS

1
Copia incolla funziona per me, il che è fantastico. Mi stavo stancando della roba per creare storyboard di Apple.
okysabeni

78

UITableViewController ha la proprietà automaticAdjustsScrollViewInsets dopo iOS 7. La visualizzazione tabella potrebbe già avere contentOffset, in genere (0, -64).

Quindi il modo giusto per mostrare refreshControl dopo aver iniziato a programmare l'aggiornamento è aggiungere l'altezza di refreshControl al contentOffset esistente.

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

ciao, grazie mille, sono anche curioso del punto magico (0, -64) che ho incontrato durante il debug.
inix

2
@inix 20 per l'altezza della barra di stato + 44 per l'altezza della barra di navigazione
Igotit

1
Invece di -64è meglio usare-self.topLayoutGuide.length
Diogo T

33

Ecco un'estensione Swift che utilizza le strategie descritte sopra.

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

Funziona per UITableView.
Juan Boero

10
Consiglierei di metterla sendActionsForControlEvents(UIControlEvents.ValueChanged)alla fine di questa funzione, altrimenti la logica della logica di aggiornamento effettiva non verrà eseguita.
Colin Basnett,

Questo è di gran lunga il modo più elegante per farlo. Compreso il commento di Colin Basnett per una migliore funzionalità. può essere utilizzato nell'intero progetto definendolo una volta!
JoeGalind

1
@ColinBasnett: aggiungendo quel codice (che ora è sendActions (per: UIControlEvents.valueChanged)), si ottiene un loop infinito ...
Kendall Helmstetter Gelner

15

Nessuna delle altre risposte ha funzionato per me. Farebbero sì che lo spinner venga mostrato e ruotato, ma l'azione di aggiornamento in sé non avverrebbe mai. Funziona:

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

Nota, questo esempio utilizza una vista tabella, ma potrebbe anche essere una vista raccolta.


Questo ha risolto il mio problema. Ma ora lo sto usando SVPullToRefresh, come tirarlo giù a livello di programmazione?
Gank

1
@Gank Non ho mai usato SVPullToRefresh. Hai provato a leggere i loro documenti? Sulla base dei documenti sembra abbastanza ovvio che può essere rimosso a livello di programmazione: "Se desideri attivare l'aggiornamento a livello di codice (ad esempio in viewDidAppear :), puoi farlo con: [tableView triggerPullToRefresh];" Vedi: github.com/samvermette/ SVPullToRefresh
Kyle Robson

1
Perfetto! Per me si comportava in modo strano con l'animazione, quindi l'ho semplicemente sostituita conscrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height)
user1366265

13

L'approccio già citato:

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

renderebbe visibile lo spinner. Ma non si animerebbe. L'unica cosa che ho cambiato è l'ordine di questi due metodi e tutto ha funzionato:

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

11

Per Swift 4 / 4.1

Un mix di risposte esistenti fa il lavoro per me:

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

Spero che questo ti aiuti!


7

Vedi anche questa domanda

UIRefreshControl non mostra spinoso quando si chiama beginRefreshing e contentOffset è 0

A me sembra un bug, perché si verifica solo quando la proprietà contentOffset di tableView è 0

L'ho risolto con il seguente codice (metodo per 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){

        }];

    }
}

1
Questo non è vero, ho due diversi UIViewController che hanno entrambi un contentOffset di 0 su viewDidLoad e uno di loro abbassa correttamente il refreshControl dopo aver chiamato [self.refreshControl beginRefreshing] e l'altro no: /
simonthumper

La documentazione non dice nulla sulla visualizzazione del controllo beginRefreshing, solo che il suo stato cambia. A mio modo di vedere, serve per impedire di avviare l'azione di aggiornamento due volte, in modo che un aggiornamento chiamato programmaticamente sia ancora in esecuzione, un'azione avviata dall'utente non ne avvia un'altra.
Koen.

Ho notato problemi con l'utilizzo del metodo setContentOffset: animated, quindi questa soluzione ha funzionato per me.
Marc Etcheverry

7

Ecco Swift 3 e le estensioni successive che mostrano lo spinner e lo animano.

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

2
Di tutte le risposte sopra, questa è stata l'unica che sono riuscito a far funzionare su iOS 11.1 / xcode 9.1
Bassebus

1
In asyncAfterrealtà è ciò che fa funzionare l'animazione dello spinner (iOS 12.3 / Xcode 10.2)
francybiga

4

Funziona perfettamente per me:

Swift 3:

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

4

Per Swift 5, per me l'unica cosa che mancava era chiamare refreshControl.sendActions(.valueChanged). Ho fatto un'estensione per renderlo più pulito.

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

}

3

Oltre alla soluzione @Dymitry Shevchenko.

Ho trovato una bella soluzione a questo problema. Puoi creare un'estensione per UIRefreshControlquel metodo di sovrascrittura:

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

È possibile utilizzare la nuova classe impostando la classe personalizzata in Identity Inspector per il controllo dell'aggiornamento in Interface Builder.


3

Fort Swift 2.2+

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

L'ho unito alla risposta accettata poiché è solo un aggiornamento.
Tudor

3

Se usi Rxswift per swift 3.1, puoi usare di seguito:

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

Funziona per swift 3.1, iOS 10.


1
È il sendActionstrigger rxche rende questa risposta correlata al RxSwiftcaso in cui qualcuno si
chieda

Ho dovuto utilizzare l'istanza di refreshControl da tableViewController, non da tableView. Inoltre, quel setContentOffset non ha funzionato per me che mira a iOS10. Questo funziona comunque: self.tableView.setContentOffset(CGPoint(x:0, y:self.tableView.contentOffset.y - (refreshControl.frame.size.height)), animated: true)
nmdias

1

testato su Swift 5

usa questo in 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() 
    }
}

0

Uso la stessa tecnica per mostrare all'utente "i dati sono aggiornati". Un utente dei risultati porta l'app dallo sfondo e feed / elenchi verranno aggiornati con l'interfaccia utente come gli utenti estraggono le tabelle per aggiornarsi. La mia versione contiene 3 cose

1) Chi manda "sveglia"

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

2) Observer in UIViewController

- (void)viewDidLoad {
    [super viewDidLoad];

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

3) Il protocollo

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

risultato

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.