Question

I am using the code below which is called within a UIViewController with embedded UITableView. It goes through a list of places, creating NSURL & passing this to NSXMLParser. Everything works with this as expected.

However, I would like for if the user clicks on a back button, that not only is the UIViewController dismissed and the user returned to the previous UIViewController which I have already, but I would like for the processing associated with the for loop that is occurring within -(void)getInfo and NSXMLParser to be terminated immediately if indeed it is still running.

In most scenarios, the processing would be complete, and this will not be an issue, however, there are possible instances where this may take a little longer, and I do not wish for the code to continue to run after pressing back, as there is then no need for the data, and thus would simply be wasting resource, and potentially jam the UI when returning to the previous UIViewController due to the processing of the multiple NSURL s.

-(void)getInfo{
    NSDictionary *places = [[NSDictionary alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"places" ofType:@"plist"]];
    NSArray *placescc = [[NSArray alloc] initWithArray:places.allKeys];
    __block NSUInteger placewaiting = placescc.count;
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSDictionary *savedLocs = [[defaults objectForKey:@"savedLocs"]mutableCopy]; //got savedLocs
    NSDictionary *thisLoc = [[savedLocs objectForKey:_LocId]mutableCopy]; //got this Loc

    lastInfoCount = 0; // set default
    if ([[thisLoc objectForKey:@"Infos"]count]){
        lastInfoCount = [[thisLoc objectForKey:@"Infos"]count]; // set saved count if we have one
    }

    for (NSString *place in placescc) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://url.com/%@/%@",place, LocIdString]];
            NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url];

            dispatch_async(dispatch_get_main_queue(), ^{
                [parser setDelegate:self];
                [parser setShouldResolveExternalEntities:NO];
                [parser parse];

                placewaiting = placewaiting-1;
                if (placewaiting == 0){

                    [self.tableView reloadData];
                    [self.activityIndicator stopAnimating];
                    lastRefresh = [NSString stringWithFormat:@"%@",[NSDate date]];
                }
            });
        });
    }
}
Was it helpful?

Solution

I'd suggest using NSOperationQueue and NSBlockOperation then you can cancel the operation in dealloc (or possibly viewWillDisappear) and within the block executing on GCD thread test isCanceled and exit / skip UI update. However you must ensure weak references to view controller or the execution block will retain the controller. Something like this:

// @property(nonatomic,weak) NSBlockOperation *backgroundOperation;

+ (NSOperationQueue*)operationQueue
{
    static dispatch_once_t onceToken;
    static NSOperationQueue *operationQueue;
    dispatch_once(&onceToken, ^{
        operationQueue = [[NSOperationQueue alloc] init];
    });

    return operationQueue;
}

- (NSOperationQueue*)operationQueue
{
    return [[self class] operationQueue];
}

- (void)dealloc
{
    [self.backgroundOperation cancel];
}

- (void)getInfo
{
    NSDictionary *places = [[NSDictionary alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"places" ofType:@"plist"]];
    NSArray *placescc = [[NSArray alloc] initWithArray:places.allKeys];
    __block NSUInteger placewaiting = placescc.count;
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSDictionary *savedLocs = [[defaults objectForKey:@"savedLocs"]mutableCopy]; //got savedLocs
    NSDictionary *thisLoc = [[savedLocs objectForKey:_LocId]mutableCopy]; //got this Loc

    lastInfoCount = 0; // set default
    if ([[thisLoc objectForKey:@"Infos"]count]){
        lastInfoCount = [[thisLoc objectForKey:@"Infos"]count]; // set saved count if we have one
    }

    // Ensure previous operation finished
    [self.backgroundOperation cancel];

    NSBlockOperation *op = [[NSBlockOperation alloc] init];
    self.backgroundOperation = op;
    __weak NSBlockOperation *weakOp = op;
    __weak typeof(self) weakSelf = self;

    [op addExecutionBlock:^{
        NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://url.com/%@/%@",place, LocIdString]];
        NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
        if (weakOp && !weakOp.isCancelled)
        {
            dispatch_async(dispatch_get_main_queue(), ^{
                [parser setDelegate:weakSelf];
                [parser setShouldResolveExternalEntities:NO];
                [parser parse];

                placewaiting = placewaiting-1;
                if (placewaiting == 0){

                    [weakSelf.tableView reloadData];
                    [weakSelf.activityIndicator stopAnimating];
                    lastRefresh = [NSString stringWithFormat:@"%@",[NSDate date]];
                }
            });
        }
    }];

    [[self operationQueue] addOperation:op];
}

Note that weakSelf, weakOp and backgroundOperation property will auto-null when their referenced objects are released so if UI update code in block ever executes after view controller dismissed weakSelf.tableView/activityIndicator will be null and a no op. In your case since most of the work is in retrieving and parsing the data synchronously so that won't be aborted but the above code means the view controller will be released immediately and UI updates on the view controller become no-ops.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top