UIRefreshControl - beginRefreshing não funciona quando UITableViewController está dentro de UINavigationController

120

Eu configurei um UIRefreshControl em meu UITableViewController (que está dentro de um UINavigationController) e funciona como esperado (ou seja, puxar para baixo dispara o evento correto). No entanto, se eu invocar programaticamente o beginRefreshingmétodo de instância no controle de atualização, como:

[self.refreshControl beginRefreshing];

Nada acontece. Deve animar para baixo e mostrar o botão giratório. O endRefreshingmétodo funciona corretamente quando eu chamo isso após a atualização.

Eu criei um projeto de protótipo básico com esse comportamento e ele funciona corretamente quando meu UITableViewController é adicionado diretamente ao controlador de visualização raiz do delegado do aplicativo, por exemplo:

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

Mas se eu adicionar o tableViewControllera um UINavigationController primeiro e, em seguida, adicionar o controlador de navegação como o rootViewController, o beginRefreshingmétodo não funcionará mais. Por exemplo

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

Minha impressão é que isso tem algo a ver com as hierarquias de visualização aninhadas dentro do controlador de navegação que não funcionam bem com o controle de atualização - alguma sugestão?

obrigado

uau
fonte

Respostas:

195

Parece que se você começar a atualizar programaticamente, terá que rolar a visualização da tabela por conta própria, digamos, alterando o deslocamento de conteúdo

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

Eu imagino que a razão para isso é que pode ser indesejável rolar para o controle de atualização quando o usuário está no meio / fundo da visualização da tabela.

Versão do Swift 2.2 por @muhasturk

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

Em suma, para manter este portátil, adicione esta extensão

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)        
    }
}
Dmitry Shevchenko
fonte
6
Isso é estranho, em meus testes, endRefreshing ajusta o deslocamento conforme necessário
Dmitry Shevchenko
9
BTW, se estiver usando o layout automático, você pode substituir a linha na resposta por: [self.tableView setContentOffset: CGPointMake (0, -self.topLayoutGuide.length) animated: YES];
Eric Baker
4
@EricBaker Eu acredito que não vai dar certo. Nem todos os UITableViewControllers mostram barras de navegação. Isso levaria a um topLayoutGuide de comprimento 20 e um deslocamento muito pequeno.
Fábio Oliveira
3
@EricBaker você pode usar: [self.tableView setContentOffset: CGPointMake (0, self.topLayoutGuide.length -self.refreshControl.frame.size.height) animated: YES];
ZYiOS
1
Copiar e colar funciona para mim, o que é incrível. Eu estava ficando cansado das coisas do construtor de storyboard da Apple.
okysabeni
78

UITableViewController possui a propriedade automaticallyAdjustsScrollViewInsets após o iOS 7. A visualização da tabela já pode ter contentOffset, geralmente (0, -64).

Portanto, a maneira certa de mostrar refreshControl depois de começar a atualizar de maneira programada é adicionar a altura de refreshControl ao contentOffset existente.

 [self.refreshControl beginRefreshing];
 [self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl.frame.size.height) animated:YES];
Deixa comigo
fonte
oi, muito obrigado, também estou curioso sobre o ponto mágico (0, -64) que conheci durante a depuração.
inix
2
@inix 20 para a altura da barra de status + 44 para a altura da barra de navegação
Igotit
1
Em vez de -64é melhor usar-self.topLayoutGuide.length
Diogo T
33

Aqui está uma extensão do Swift usando as estratégias descritas acima.

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
fonte
Funciona para UITableView.
Juan Boero
10
Eu recomendaria colocar sendActionsForControlEvents(UIControlEvents.ValueChanged)no final desta função, caso contrário, a lógica de atualização real não será executada.
Colin Basnett de
Esta é de longe a maneira mais elegante de fazer isso. Incluindo o comentário de Colin Basnett para uma melhor funcionalidade. ele pode ser usado em todo o projeto, definindo-o uma vez!
JoeGalind de
1
@ColinBasnett: Adicionar esse código (que agora é sendActions (para: UIControlEvents.valueChanged)), resulta em um loop infinito ...
Kendall Helmstetter Gelner
15

Nenhuma das outras respostas funcionou para mim. Eles fariam com que o botão giratório mostrasse e girasse, mas a ação de atualização em si nunca aconteceria. Isso 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];

Observe que este exemplo usa uma visão de tabela, mas poderia muito bem ser uma visão de coleção.

Kyle Robson
fonte
Isso resolveu meu problema. Mas agora estou usando SVPullToRefresh, como puxá-lo programaticamente?
Gank
1
@Gank Eu nunca usei SVPullToRefresh. Você já tentou ler seus documentos? Parece bastante óbvio, com base na documentação, que pode ser baixado programaticamente: "Se desejar acionar a atualização programaticamente (por exemplo, em viewDidAppear :), você pode fazer isso com: [tableView triggerPullToRefresh];" Consulte: github.com/samvermette/ SVPullToRefresh
Kyle Robson
1
Perfeito! Estava estranho para mim com a animação, então eu simplesmente a substituí porscrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height)
user1366265
13

A abordagem já mencionada:

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

tornaria o botão giratório visível. Mas não animava. A única coisa que mudei foi a ordem desses dois métodos e tudo funcionou:

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

Para Swift 4 / 4.1

Uma combinação de respostas existentes faz o trabalho para mim:

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

Espero que isto ajude!

Isaac Bosca
fonte
7

Veja também esta questão

UIRefreshControl não mostra spiny ao chamar beginRefreshing e contentOffset é 0

Parece um bug para mim, porque só ocorre quando a propriedade contentOffset de tableView é 0

Corrigi isso com o seguinte código (método para o 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
fonte
1
Isso não é verdade, eu tenho dois UIViewControllers diferentes, ambos com contentOffset 0 em viewDidLoad e um deles puxa corretamente o refreshControl ao chamar [self.refreshControl beginRefreshing] e o outro não: /
simonthumper
A documentação não diz nada sobre a exibição do controle ativado beginRefreshing, apenas que seu estado muda. A meu ver, é para evitar que a ação de atualização seja iniciada duas vezes, de modo que, se uma atualização chamada programaticamente ainda estiver em execução, uma ação iniciada pelo usuário não iniciará outra.
Koen.
Observei problemas com o uso do método setContentOffset: animated, portanto, essa solução funcionou para mim.
Marc Etcheverry
7

Aqui está o Swift 3 e uma extensão posterior que mostra o spinner e também o 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
fonte
2
De todas as respostas acima, esta foi a única que consegui trabalhar no iOS 11.1 / xcode 9.1
Bassebus
1
A asyncAfteré realmente o que torna o trabalho de animação girador (iOS 12,3 / Xcode 10.2)
francybiga
4

Funciona perfeitamente para mim:

Swift 3:

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

Para o Swift 5, para mim, só faltou ligar refreshControl.sendActions(.valueChanged). Fiz uma extensão para torná-la mais limpa.

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
fonte
3

Além da solução @Dymitry Shevchenko.

Eu encontrei uma boa solução para esse problema. Você pode criar uma extensão para UIRefreshControlesse método de substituição:

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

Você pode usar a nova classe definindo uma classe personalizada no Identity Inspector para controle de atualização no Interface Builder.

Lyzkov
fonte
3

Fort Swift 2.2+

    self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.frame.size.height), animated: true)
muhasturk
fonte
Eu mesclei isso com a resposta aceita, pois é apenas uma atualização.
Tudor
3

Se você usa Rxswift para swift 3.1, pode usar abaixo:

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

Isso funciona para o swift 3.1, iOS 10.

jkyin
fonte
1
É o sendActionsgatilho rxque torna esta resposta relacionada ao RxSwiftcaso de alguém estar se perguntando à primeira vista
carbonr
Tive que usar a instância refreshControl do tableViewController, não o tableView. Além disso, esse setContentOffset não funcionou para mim visando iOS10. Este funciona no entanto: self.tableView.setContentOffset(CGPoint(x:0, y:self.tableView.contentOffset.y - (refreshControl.frame.size.height)), animated: true)
nmdias
1

testado em Swift 5

use isso em 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
fonte
0

Eu uso a mesma técnica para mostrar ao usuário o sinal visual "dados são atualização". Um resultado do usuário traz o aplicativo do fundo e os feeds / listas serão atualizados com a interface do usuário, como os usuários puxam as tabelas para se refrescar. Minha versão contém 3 coisas

1) Quem manda "acordar"

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

2) Observador em UIViewController

- (void)viewDidLoad {
    [super viewDidLoad];

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

3) O 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
fonte