UIRefreshControl - beginRefreshing не работает, когда UITableViewController находится внутри UINavigationController

120

Я установил UIRefreshControl в моем UITableViewController (который находится внутри UINavigationController), и он работает, как ожидалось (т. Е. При спуске срабатывает правильное событие). Однако, если я программно вызываю beginRefreshingметод экземпляра в элементе управления обновлением, например:

[self.refreshControl beginRefreshing];

Ничего не произошло. Он должен оживить и показать спиннер. endRefreshingМетод работает должным образом , когда я звоню , что после обновления.

Я разработал базовый прототип проекта с таким поведением, и он работает правильно, когда мой UITableViewController добавляется непосредственно в корневой контроллер представления делегата приложения, например:

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

Но если я tableViewControllerсначала добавлю к UINavigationController, а затем добавлю контроллер навигации в качестве rootViewController, beginRefreshingметод перестанет работать. Например

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

Я считаю, что это как-то связано с иерархиями вложенных представлений в контроллере навигации, которые не очень хорошо сочетаются с элементом управления обновлением - есть предложения?

Спасибо

ошеломляет
источник

Ответы:

195

Похоже, если вы начнете обновляться программно, вам придется самостоятельно прокручивать представление таблицы, скажем, изменяя contentoffset

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

Я предполагаю, что причина этого в том, что было бы нежелательно прокручивать элемент управления обновлением, когда пользователь находится в середине / внизу представления таблицы?

Версия Swift 2.2 от @muhasturk

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

Вкратце, чтобы сохранить переносимость, добавьте это расширение

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
Странно, в моих тестах endRefreshing подстраивает смещение по мере необходимости
Дмитрий Шевченко
9
Кстати, если вы используете автоматический макет, вы можете заменить строку в ответе на это: [self.tableView setContentOffset: CGPointMake (0, -self.topLayoutGuide.length) animated: YES];
Эрик Бейкер
4
@EricBaker: Думаю, так не пойдет. Не все UITableViewController показывают панели навигации. Это приведет к тому, что topLayoutGuide будет иметь длину 20 и смещение будет слишком маленьким.
Фабио Оливейра
3
@EricBaker вы можете использовать: [self.tableView setContentOffset: CGPointMake (0, self.topLayoutGuide.length -self.refreshControl.frame.size.height) animated: YES];
ZYiOS
1
Копипаст для меня работает, и это здорово. Я устал от компоновщика раскадровки Apple.
okysabeni
78

UITableViewController имеет автоматическиAdjustsScrollViewInsets свойство после iOS 7. Табличное представление может уже иметь contentOffset, обычно (0, -64).

Итак, правильный способ показать refreshControl после начала обновления программирования - это добавить высоту refreshControl к существующему contentOffset.

 [self.refreshControl beginRefreshing];
 [self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl.frame.size.height) animated:YES];
Я понял
источник
привет, Большое спасибо, мне также интересно узнать о волшебной точке (0, -64), которую я встретил при отладке.
inix
2
@inix 20 для высоты строки состояния + 44 для высоты панели навигации
Igotit
1
Вместо этого -64лучше использовать-self.topLayoutGuide.length
Diogo T
33

Вот расширение Swift, использующее описанные выше стратегии.

extension UIRefreshControl {
    func beginRefreshingManually() {
        if let scrollView = superview as? UIScrollView {
            scrollView.setContentOffset(CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height), animated: true)
        }
        beginRefreshing()
    }
}
Крис Вагнер
источник
Работает для UITableView.
Хуан Боэро
10
Я бы рекомендовал поставить sendActionsForControlEvents(UIControlEvents.ValueChanged)эту функцию в конце, иначе логика логики обновления не будет запущена.
Colin Basnett
Это, безусловно, самый элегантный способ сделать это. Включая комментарий Колина Баснета для улучшения функциональности. его можно использовать для всего проекта, определив его один раз!
JoeGalind
1
@ColinBasnett: Добавление этого кода (который теперь называется sendActions (для: UIControlEvents.valueChanged)) приводит к бесконечному циклу ...
Кендалл Хельмштеттер Гельнер
15

Ни один из других ответов не помог мне. Они заставят счетчик показывать и вращаться, но само действие обновления никогда не произойдет. Это работает:

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

Обратите внимание: в этом примере используется табличное представление, но с тем же успехом оно могло быть и представлением коллекции.

Кайл Робсон
источник
Это решило мою проблему. А вот сейчас пользуюсь SVPullToRefresh, как программно сваливать?
Gank
1
@Gank Я никогда не использовал SVPullToRefresh. Вы пробовали читать их документы? Судя по документации, кажется совершенно очевидным, что его можно вытащить программно: «Если вы хотите запустить обновление программно (например, в viewDidAppear :), вы можете сделать это с помощью: [tableView triggerPullToRefresh];» См.: Github.com/samvermette/ SVPullToRefresh
Кайл Робсон
1
Отлично! Для меня это scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height)
выглядело
13

Уже упомянутый подход:

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

сделает спиннер видимым. Но он не оживал. Единственное, что я изменил, - это порядок этих двух методов, и все заработало:

[self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl.frame.size.height) animated:YES];
[self.refreshControl beginRefreshing];
ancajic
источник
11

Для Swift 4 / 4.1

Комбинация существующих ответов делает работу за меня:

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

Надеюсь это поможет!

Исаак Боска
источник
7

См. Также этот вопрос

UIRefreshControl не показывает шип при вызове beginRefreshing и contentOffset равен 0

Для меня это похоже на ошибку, потому что она возникает только тогда, когда свойство contentOffset tableView равно 0

Я исправил это с помощью следующего кода (метод для 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
Это неправда, у меня есть два разных UIViewController, оба из которых имеют contentOffset 0 при viewDidLoad, и один из них правильно вытаскивает refreshControl при вызове [self.refreshControl beginRefreshing], а другой нет: /
simonthumper
В документации ничего не говорится об отображении элемента управления beginRefreshing, только об изменении его состояния. Насколько я понимаю, это сделано для того, чтобы не запускать действие обновления дважды, чтобы, если программно вызываемое обновление все еще работало, действие, инициированное пользователем, не запускало другое.
Коэн.
Я заметил проблемы с использованием метода setContentOffset: animated, поэтому это решение сработало для меня.
Marc Etcheverry
7

Вот расширение Swift 3 и более поздних версий, которое показывает счетчик, а также его анимацию.

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
Из всех вышеперечисленных ответов это был единственный, над которым я смог работать над iOS 11.1 / xcode 9.1
Bassebus
1
На asyncAfterсамом деле это то, что заставляет анимацию счетчика работать (iOS 12.3 / Xcode 10.2)
francybiga
4

Мне это отлично подходит:

Swift 3:

self.tableView.setContentOffset(CGPoint(x: 0, y: -self.refreshControl!.frame.size.height - self.topLayoutGuide.length), animated: true)
Meilbn
источник
4

В Swift 5 мне не хватало только звонка refreshControl.sendActions(.valueChanged). Я сделал расширение, чтобы было чище.

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
источник
3

В дополнение к решению @Dymitry Shevchenko.

Я нашел хорошее решение этой проблемы. Вы можете создать расширение для UIRefreshControlэтого метода перезаписи:

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

Вы можете использовать новый класс, установив собственный класс в Identity Inspector для управления обновлением в Interface Builder.

lyzkov
источник
3

Форт Свифт 2.2+

    self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.frame.size.height), animated: true)
muhasturk
источник
Я объединил это с принятым ответом, поскольку это просто обновление.
Tudor
3

Если вы используете Rxswift для swift 3.1, можете использовать ниже:

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

Это работает для Swift 3.1, iOS 10.

jkyin
источник
1
Это sendActionsтриггер, rxкоторый делает этот ответ связанным с тем, что RxSwiftкому-то интересно с первого взгляда
carbonr
Мне пришлось использовать экземпляр refreshControl из tableViewController, а не из tableView. Кроме того, этот setContentOffset не работал у меня с таргетингом на iOS10. Однако этот работает: self.tableView.setContentOffset(CGPoint(x:0, y:self.tableView.contentOffset.y - (refreshControl.frame.size.height)), animated: true)
nmdias
1

протестировано на Swift 5

использовать это в 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

Я использую ту же технику для отображения визуального знака "данные обновляются". В результате пользователь извлекает приложение из фона, а каналы / списки будут обновляться с помощью пользовательского интерфейса, как если бы пользователь сам обновлял таблицы. Моя версия содержит 3 вещи

1) Кто отправляет "просыпаться"

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

2) Наблюдатель в UIViewController

- (void)viewDidLoad {
    [super viewDidLoad];

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

3) Протокол

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

результат

WINSergey
источник