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

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

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

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

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.


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

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!


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

        }];

    }
}

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

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)

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

}

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.


3

Fort Swift 2.2+

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

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.


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

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.