UIRefreshControl - 当UITableViewController嵌套在UINavigationController中时,beginRefreshing无法正常工作

132

我在我的UITableViewController中设置了一个UIRefreshControl(它在一个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;

我的感觉是这与导航控制器中嵌套的视图层次结构与刷新控件不兼容有关 - 有什么建议吗?

谢谢

15个回答

206

看起来,如果你以编程方式开始刷新,你就必须自己滚动表视图,比如通过改变contentoffset。

[self.tableView setContentOffset:CGPointMake(0, -self.refreshControl.frame.size.height) animated:YES];
我猜这样做的原因是,当用户在表视图的中间/底部时滚动到刷新控件可能会不太理想。
由@muhasturk提供的Swift 2.2版本。
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会根据需要调整偏移量。 - Dmitry Shevchenko
有时候会出现这种情况,有时候又不会。可能与UINavigationController的问题有关?不太确定 :S - wows
9
顺便提一下,如果您正在使用自动布局,可以将答案中的行替换为以下内容:[self.tableView setContentOffset:CGPointMake(0, -self.topLayoutGuide.length) animated:YES]; - Eric Baker
4
我认为那样行不通。并不是所有的UITableViewControllers都显示导航栏。这会导致topLayoutGuide长度为20,偏移量太小。 - Fábio Oliveira
3
你可以使用以下代码:[self.tableView setContentOffset:CGPointMake(0, self.topLayoutGuide.length -self.refreshControl.frame.size.height) animated:YES]; 这行代码可以使表视图滚动到指定位置,让刷新控件与顶部布局指南对齐。 - ZYiOS
显示剩余5条评论

80

iOS 7之后,UITableViewController类有了automaticallyAdjustsScrollViewInsets属性。表视图可能已经有了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

36

这是一个使用上述策略的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的代码。 - Juan Boero
10
建议将 sendActionsForControlEvents(UIControlEvents.ValueChanged) 放在该函数的末尾,否则实际的刷新逻辑将不会执行。 - Colin Basnett
这绝对是最优雅的方法。包括Colin Basnett的评论以获得更好的功能性。只需定义一次即可在整个项目中使用! - JoeGalind
1
@ColinBasnett:添加该代码(现在是sendActions(for: UIControlEvents.valueChanged)),会导致无限循环... - Kendall Helmstetter Gelner

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];”请参见:https://github.com/samvermette/SVPullToRefresh - Kyle Robson
2
太好了!但是动画对我来说有些奇怪,所以我只是用scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height)替换了它。 - user1366265

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

11

适用于Swift 4/4.1

一些现有答案的结合对我起了作用:

refreshControl.beginRefreshing()
tableView.setContentOffset(CGPoint(x: 0, y: tableView.contentOffset.y - (refreshControl.frame.size.height)), animated: true)
希望这能有所帮助!

8

还可以参考这个问题:

调用beginRefreshing时,UIRefreshControl不显示旋转效果且contentOffset为0

在我看来,这似乎是一个bug,因为它只会在tableView的contentOffset属性为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
这不是真的,我有两个不同的UIViewControllers,在viewDidLoad时都有contentOffset为0,其中一个在调用[self.refreshControl beginRefreshing]后正确地拉下了refreshControl,而另一个没有 :/ - simonthumper
文档没有说明在 beginRefreshing 时如何显示控件,只是说明其状态会改变。我认为这是为了防止重复启动刷新操作,以便即使程序调用了刷新,用户发起的操作也不会再次启动另一个刷新。 - Koen.
我注意到使用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

5
对于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)
    }

}

调用发送操作方法是我唯一的解决办法! - jegadeesh
触发器无限循环。 - Mishka

5

它对我来说完美运作:

Swift 3:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接