1

I'm having a problem where I'm unable to update UI when performing synchronous downloads. I would expect that using synchronous APIs would ensure that code executes in order (which it doesn't seem to be doing), which is really confusing me.

The following code is in a UICollectionView's didSelectItemAtIndexPath and is not wrapped in any asynchronous block or anything.

Any ideas on what I can do to be able to update the UI (most importantly a progress indicator) as these tasks occur? I think that the way it is currently laid out should work, but for some reason it's not able to update until the code has all 'executed'.

if ([internetReachable isReachable]) {
    //does not become visible until after
    self.circleProgress.alpha = 1.0;

    //lots of downloading and saving with NSData dataWithContentsOfURL followed by this: 

    for (int i = 1; i < pages.count; i++) {
        NSString *number;
        if (i < 10) {
            number = [NSString stringWithFormat:@"00%d", i];
        }
        else if (i < 100) {
            number = [NSString stringWithFormat:@"0%d", i];
        }
        else {
            number = [NSString stringWithFormat:@"%d", i];
        }
        NSURL *imageURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://books.hardbound.co/%@/%@-%@.png", slug, slug, number]];
        NSData *imageData = [NSData dataWithContentsOfURL:imageURL];

        [df setObject:imageData forKey:[NSString stringWithFormat:@"%@-%@", slug, number]];

        CGFloat progress = ((CGFloat)i / pages.count);
//only runs for the last iteration, rather than calling the method to update the progress indicator each iteration and allowing it to update before going back to the next iteration as I would expect
        [self updateProgressBarWithAmount:[NSNumber numberWithFloat:progress]];

        NSLog(@"progress after: %f", self.circleProgress.progress);
    }   
}
John
  • 2,820
  • 3
  • 30
  • 50
  • Where are you setting value to progress bar? show updateProgressBarWithAmount method. have you used [progressBar setProgress:[NSNumber numberWithFloat:progress] animated:YES]; – Avijit Nagare Aug 07 '15 at 03:17
  • Why do you block the main thread in this manner instead of async? – Andrew Aug 07 '15 at 03:36

2 Answers2

3

UI can only be executed on the main thread. Since the main thread is busy doing the downloading, it can't update the UI. It's almost never a good idea to perform any long running operations on the main thread. You should make the download asynchronous, and update the UI on the main thread.

The loop in the code you posted will only be executed after lots of downloading and saving with NSData dataWithContentsOfURL is performed, all the while the application will be unresponsive, and that's very poor UX. Take a look at this question for a much better implementation of a progress bar.

Community
  • 1
  • 1
p4sh4
  • 3,292
  • 1
  • 20
  • 33
  • I see. I was under the impression that NSData dataWithContentsOfURL would have to download before moving on, and by the point the next line of code was executed, you can assume the thread is clear.What you're saying is that code continues to execute on the main thread with no effect because it's actually still downloading? – Harrison Weinerman Aug 07 '15 at 03:21
  • You are right, and most likely the reason why it's not updating is some error in your `updateProgressBarWithAmount` method. But, in any case, this is the wrong way to make a progress bar. So wrong, in fact, that it might not get accepted into the App Store. What if the URL is inaccessible? Your app will just hang there... What about the NSLog, does it print on every iteration? – p4sh4 Aug 07 '15 at 03:28
  • Since you are continuously blocking the main thread with downloads, I believe what he is saying is that there will be no time for the UI to actually get updated despite whatever you do in `updateProgressBarWithAmount` – Andrew Aug 07 '15 at 03:32
  • @SantaClaus This is not really what I was saying, I was referring to the comment before the loop which I assumed means that there is more synchronous code being executed there which is omitted. I think what you're saying is not correct, if the UI uses the main thread shouldn't it wait for the UI update to finish and only then execute the consecutive `dataWithContentsOfURL` in the next iteration? – p4sh4 Aug 07 '15 at 03:43
  • @p4sh4 I've actually been unsure of this myself but I can't seem to find any good resources that would help. I don't think `setProgress:animated` blocks the main thread though. Animations never block the main thread, right? I guess I've confused myself haha. – Andrew Aug 07 '15 at 03:47
  • 1
    But the lesson of the story is - don't block the main thread with long running operations. – Andrew Aug 07 '15 at 03:48
  • 1
    According to [this doc](https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/AnimatingViews/AnimatingViews.html), animations that are made with block-based methods run on another thread. In the case of `setProgress:animated`, I would assume that the actual setting does block the main thread, but the animation doesn't. In any case, if we learn the lesson of the story, it's a non-issue :). But if someone is familiar with the innards of iOS I'd love to hear a proper answer! – p4sh4 Aug 07 '15 at 03:59
  • 1
    @p4sh4 Yes, I'd love to hear what's really happening here. If I get a chance and remember to, I'll try to mess around a little with blocking the main thread (with sleep(100) for example) and updating ui elements such as the progress view in an empty project. – Andrew Aug 07 '15 at 04:13
  • 1
    @p4sh4 I was actually able to figure this out somewhat at least (see my answer) – Andrew Aug 07 '15 at 05:04
1

I am not by any means qualified to explain what exactly happens during each render loop and why updateProgress doesn't actually let a screen render occur before you block the main thread again, but I am able to provide a solution.

After you update the progress of the progress view, you want the changes to get rendered "right now". This means you have to tell the current run loop to run one iteration, and then return to you so you can do another long running task.

[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];

Call that whoever you want the progress view to update, and it will do a screen render and then return to you.

I got this from this answer

However, you really should be doing this asynchronously.

(Apologies for any typos, as this is being typed on my phone)

Community
  • 1
  • 1
Andrew
  • 15,357
  • 6
  • 66
  • 101
  • 1
    Cool! Too bad this is the kind of guesswork we have to do when working with closed-source APIs. Kudos for the research, but I would like to emphasize that although it's possible to use this method, no one really should :) – p4sh4 Aug 07 '15 at 05:26