Update NSWindow content while asynchronously loading a file

137 views Asked by At

In my document-based MacOS application, I have some big files that are loaded (especially recent files opened at application launch). I created ProgressController (a NSWindowController subclass) to inform user in a window that file loading is in progress. It is allocated by the makeWindowControllers method of the NSDocument subclass I use to manage documents. These are created when the user opens a file (and notably at startup when documents were displayed as the user did quit the application) and they load their content asynchronously in a background queue which in principle should not affect the main thread performance:

-(instancetype) initForURL:(NSURL *)urlOrNil withContentsOfURL:(NSURL *)contentsURL ofType:(NSString *)typeName error:(NSError *
{
  if (self = [super init]){
    ... assign some variables before loading content...
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0), ^{

            [[[NSURLSession sharedSession] dataTaskWithURL:urlOrNil completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                self.content =  [[NSMutableString alloc] initWithData:data encoding:encoder];
                dispatch_async(dispatch_get_main_queue(), ^(){
                    [self contentIsLoaded];
                });
            }] resume];
        });
  }
  return self;
}

In contentIsLoaded, the progress window is closed.

- (void) contentIsLoaded
{
    ... do something ...
    [self.progressController close];
}

This behaviour is OK, the window is displayed and closed when necessary. The problem occurs when I want to update the content of this window on the main queue. I tried setting a NSTimer but is is never fired, even though it is created in the main queue. So, in MyApplicationDelegate I created a GCD timer like this:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
             if (self->timer !=nil) dispatch_source_cancel(timer);
             self.queue =  dispatch_queue_create( "my session queue", DISPATCH_QUEUE_CONCURRENT);
             timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
             dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
             dispatch_source_set_event_handler(timer, ^{
                [self updateProgress];
             });
             dispatch_resume(timer);
            …
    }

In MyApplicationDelegate, the updateProgress method is defined as:

- (void) updateProgress
{
    dispatch_async(self.queue, ^{
        NSLog(@"timer method fired");
        dispatch_async(dispatch_get_main_queue(), ^(){
            NSLog(@"access to UI fired");
            ... update window (e.g. NSProgressIndicator)
            }
        });
        if (self.shouldCancelTimer) dispatch_source_cancel(self->timer);
    });
}

When running the application, The "Timer method fired" is logged every second. The "timer method fired" message (in the main queue) is logged once or twice only, then the logging seems suspended until the file has been loaded. Then this missing message appears several times in a row, as it was suspended before.

What did I wrong? I supposed that the background queue used for file loading should not affect the main queue and UI updates. Many applications behave like that and I need such behaviour as files in my App (strings, csv, json) can be hundreds of Mbytes!

1

There are 1 answers

0
Mattie On

For starters, have you looked into NSURLSessionTask's progress reporting APIs? There are a bunch of facilities for measuring and getting called back as data loads. It might be possible for you to avoid doing the polling, and just update your UI as these are called back - all on the main thread.

In fact, you might not even need one at all. As I recall, the networking operations carried out by NSURLSession are all done in the background anyways. I bet you can remove all of this, and just use some extra delegate callbacks from NSURLSession APIs to get this done. Way simpler too :)

Good luck!