How to update a UILabel synchronously with code in a method using setNeedsDisplay

StackOverflow https://stackoverflow.com/questions/23679312

  •  23-07-2023
  •  | 
  •  

Domanda

I am downloading a zip file from the internet and unzipping it. The process takes 15 seconds or so. I want to display some sort of time elapsed to the user. As a start I just want to update a UILabel on my _countUpView UIView when the download has finished and the unzipping begins. I believe that I need to use setNeedsDisplay on the view that holds the UILabel but I can not seem to figure out what I am doing wrong, here is my code:

 NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://URLtothiszipfile>/all.zip"]];
 NSData *slicedata= [NSData dataWithContentsOfURL:url];
 NSError *error;
 NSString *isignsPath = [NSString stringWithFormat:@"%@/iSigns",docsDir];
 if (![filemgr fileExistsAtPath:isignsPath]){    //Does directory already exist?
    if (![filemgr createDirectoryAtPath:isignsPath withIntermediateDirectories:NO attributes:nil error:&error]){
       NSLog(@"Create directory error: %@", error);
    }
 }
 thePathAndName= [NSString stringWithFormat:@"%@/iSigns/all.zip", docsDir];
 [slicedata writeToFile:thePathAndName atomically:YES];
 _countUpLab.text= @"Unzipping";
 [self displayCountUpNOW];
 NSLog(@"Unzipping");
 [SSZipArchive unzipFileAtPath:thePathAndName toDestination:isignsPath overwrite:true password:@"" error:&error];
 if (error.code!= noErr){
    NSLog(@"Unzip error");
    return;
 }else{
    NSLog(@"Unzipped successfully");
 }

And the setNeedsDisplay method is:

- (void) displayCountUpNOW {
   dispatch_async(dispatch_get_main_queue(), ^{
      [_countUpView setNeedsDisplay];
   });
}

My label does not change to "Unzipping" until NSLog shows "Unzipped successfully", about 10 seconds after I set the label to "Unzipping".

In those 10 seconds, the unzipping occurs but a timer that I want to use to update the label every second stops executing too so I can't display the elapsed time in the label either.

Please help me understand this asynchronous environment.
Carmen

EDIT-EDIT-EDIT The below code seems to work asynchronously and I even have my elapsed time indicator working since my timer isn't stopped. I couldn't find a method in SSZipArchive that works without a file so I left the save file in. How did I do Duncan? Thanks again, that is pretty slick!

ONE MORE QUESTION: What is the best way to know when an asynchronous request is still outstanding, by setting a global flag variable when the request is made and clearing it when the async process completes?

   gotALL= 0;
   _countUpLab.text= @"Downloading";
   NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://URLtothiszipfile>/all.zip"]];
   NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
   [NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *slicedata, NSError *error1){
      if ([slicedata length]> 0 && error1== nil){
         NSString *isignsPath = [NSString stringWithFormat:@"%@/iSigns",docsDir];
         if (![filemgr fileExistsAtPath:isignsPath]){    //Does directory already exist?
            NSError *error2;
            if (![filemgr createDirectoryAtPath:isignsPath withIntermediateDirectories:NO attributes:nil error: &error2]){
               NSLog(@"Create directory error: %@", error2);
               [self endCountUp];
               return;
            }
         }
         thePathAndName= [NSString stringWithFormat:@"%@/iSigns/all.zip", docsDir];
         [slicedata writeToFile:thePathAndName atomically:YES];
         _countUpLab.text= @"Unzipping";
         NSLog(@"Unzipping");
         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSError *error3;
            [SSZipArchive unzipFileAtPath:thePathAndName toDestination:isignsPath overwrite:true password:@"" error:&error3];
            dispatch_async(dispatch_get_main_queue(), ^(void) {
               if (error3.code!= noErr){
                  NSLog(@"Unzip error %@", error3.description);
                  [self endCountUp];
                  return;
               }else{
                  NSLog(@"Unzipped successfully");
                  gotALL= 1;
                  [self endCountUp];
                  return;
               }
            });
         });
      }else if ([slicedata length]== 0 && error1== nil){
         //todo yet
      }else if (error1!= nil){
         //todo yet
      }
   }];
È stato utile?

Soluzione

Lots of problems with your code. First of all, Your current code will lock up the user interface until the download is complete. That is bad, even for short downloads. If there is a network problem, this can lock up the user interface for up to 2 minutes, even for a short file.

You should download the file using an async download, not synchronous.

Second of all, UI updates only get rendered to the screen when your code returns and the app visits the event loop. Code using this approach:

  • Set label text to "doing stuff"
  • Do something time-consuming
  • Change label text to "done with stuff"

Does not work. The text "doing stuff" never shows up in the label, because the label changes aren't drawn until your code finishes and returns, and by then, you've replaced the label text with "done with stuff."

Here's what you should do instead:

Use the NSURLConnection method sendAsynchronousRequest:queue:completionHandler:. Put the code that you want to run once the download is complete in the completionHandler block.

That method handles the background stuff for you, and then runs your completion block on the main thread once the download is complete. It's a slick system method.

It should be possible to do the unzipping from the background using dispatch_async. (I'm not familiar with the SSZipArchive library, so I'm not positive that it's thread-safe, but it should be.)

The basic idea is:

  • Display a "downloading" message.
  • Create an NSURLRequest.
  • Use sendAsynchronousRequest:queue:completionHandler: to submit the request asynchronously.
  • In the completion handler:
    • Change the message to "unzipping"
    • Save the downloaded data to disk if the unzip library requires that it be saved to a file. If the zip library can do the unzipping from memory, you can skip this step.
    • Use dispatch_async to unzip the file from the default priority global queue
    • At the end of the unzip block, use dispatch_async(dispatch_get_main_queue()) to change the label to "unzip complete" or whatever you want to say. (You have to send the message to the main queue because you can't change the UI from a background thread.)

Try and figure out how to code the above approach. If you get stuck, post what you've tried as an edit to your post and we can guide you, but it's better if you try to write this code yourself. You'll learn that way rather than copy/pasting.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top