UIAlertController sometimes prevents UIRefreshControl to hide

2.8k Views Asked by At

I'm using UIRefreshControl on my tableview to update items. And the end, I show a UIAlertController to inform the user that the update was complete, and how many items were updated. Pretty straightforward, and it works fine for one thing. If I pull to refresh several times in a row, sometimes the refresh control is not dismissed, even after dismissing the alert. I need to swipe up the table to make it go away.

This is the code I use, all UI stuff is nicely done on the main thread:

    if(refreshControl.refreshing) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self refreshItems];

            dispatch_async(dispatch_get_main_queue(), ^{
                [refreshControl endRefreshing];
                [self.tableView reload];
                [self showUpdateInfo];                     
            });
        });
    }

Any idea what could cause this?

EDIT: This is how I create the refresh control in code (in viewDidLoad):

UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
refreshControl.attributedTitle   = [[NSAttributedString alloc] initWithString: @"Checking for updates…"];
[refreshControl addTarget: self
                   action: @selector(refreshOutdatedItems)
         forControlEvents: UIControlEventValueChanged];

self.refreshControl = refreshControl;
6

There are 6 best solutions below

12
On BEST ANSWER

I believe it really has to do with the tableView not scrolling back up before the UIAlertController is presented. I tried to set the delay for showUpdateInfo method and that seems to worked. I guess when the user only pulls once it will take half a second to show the UIAlertController check if that helps. Here is my code

- (void)refreshOutdatedItems
{
    NSLog(@"refresh");

    if (self.refreshControl.refreshing) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            for (double i = 0 ; i < 100000000; i++){

            };

            dispatch_async(dispatch_get_main_queue(), ^{

                [self.refreshControl endRefreshing];
                [self performSelector:@selector(showUpdateInfo) withObject:nil afterDelay:0.5];

            });
        });
    }
}

Let me know if helps.

0
On

Add this extension to your view controller:

extension UIViewController {
    func showAlert(withTitle title: String, andMessage message: String, completion: (() -> Void)? = nil) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: L10n.General.ok, style: .default, handler: { _ in
            self.dismiss(animated: true, completion: nil)
        }))
        self.present(alert, animated: true, completion: completion)
    }
}

then use the following code to show the alert. In the completion block of the alert the refresh control is stopped and will fade out smoothly.

showAlert(withTitle: "My Title", andMessage: "There was an error", completion: { 
      refreshControl.endRefreshing() 
 })
1
On

Those works for me:

    refreshControl.endRefreshing()
    tableView.setContentOffset(CGPoint.zero, animated: true)
    -- show alert than --
2
On

Delaying by an arbitrary time is not a very clean solution. Implementing the delay by counting up a double to 10 million also is a bit of a hack in my opinion.

What I ended up doing instead is similar to this pseudo-Swift:

let alert = UIAlertController(...)
present(alert, animated: true) {
    refreshControl.endRefreshing()
}

It delays the call to endRefreshing() until the alert is presented.

My actual code using Swift 3 and PromiseKit looks more like this:

@IBAction func onRefresh() {
    Repository.getNewData().then { data in
        self.data = data
        self.tableView.reloadData()
    }.recover { error in
        // Return a promise to make sure endRefreshing isn't called until the alert is presented.
        return self.presentErrorAlert(message: "Some user-friendly message")
    }.always {
        self.refreshControl?.endRefreshing()
    }
}

private func presentErrorAlert(message: String) -> Promise<Void> {
    return Promise<Void> { fulfill, _ in
        let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        self.present(alert, animated: true) {
            fulfill()
        }
    }
}
0
On

Simply reset the content offset of table view:

- (void)endRefreshControl {
     [self.tableView setContentOffset:CGPointMake(0, -self.refreshControl.bounds.size.height) animated:YES];
     [self.refreshControl endRefreshing];
}

When your task completes, call [self endRefreshControl];
rather than [self.refreshControl endRefreshing];

0
On

This seems to be a bug in Xcode. Even Johan Levin's solution sometimes seems to hang in Xcode 9.2.

It's not exactly what you're looking for, but it's a workaround: Don't use the UIAlertController. Just place the text you had in UIAlertController into the UIRefreshControl like so:

refreshControl?.attributedTitle = NSAttributedString(string: "Pull to reload all reminders", attributes: [NSAttributedStringKey.foregroundColor: UIColor(red: 255.0/255.0, green: 182.0/255.0, blue: 8.0/255.0, alpha: 1.0)])