Progress indicator inside enumerateObjectsUsingBlock


#1

I’ve been trying to put this from your sample:
//Set up our callback using threads
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
/*
* Put your long-running, more-exciting-than-sleeping-for-3-seconds task here!
*/
[NSThread sleepForTimeInterval:3.0f];
[self performSelectorOnMainThread:@selector(finishIndicator:) withObject:actIndicator waitUntilDone:NO];
});

into a category like this:

  • (void)sync:(BOOL)update
    {
    SEssentialsProgressIndicator *progIndicator = [SEssentialsProgressIndicator
    progressIndicatorOfType:SEssentialsIndicatorTypeLinearContinuous
    withFrame:CGRectMake(200, 300, 400, 40)];
    progIndicator.style.theme.primaryTintColor = [UIColor colorWithWhite:0.1 alpha:1];

[[[UIApplication sharedApplication] keyWindow] addSubview:progIndicator];

SCWebServiceStore *webServiceStore = (SCWebServiceStore *)[webServiceDef generateCompatibleDataStore];
[webServiceStore asynchronousFetchObjectsWithOptions:nil success: ^(NSArray *results)
{
NSLog(@“Successfully fetched %i objects!”, results.count);

[results enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(@“Inserting new record: %i of %i”, idx+1, results.count);

// This is not working from here:
progIndicator.progress = idx+1 / results.count;
// nor this:
[self performSelectorOnMainThread:@selector(updateProgressByStep:) withObject:progIndicator waitUntilDone:YES];
}];
}
failure: ^(NSError *error)
{
}];
}

  • (void)updateProgressByStep:(SEssentialsProgressIndicator*)progIndicator
    {
    progIndicator.progress += 0.01;
    }

How would I be able to use it in this scenario?

Wg


#2

From a quick scan of your code, you appear to be dividing an integer (idx+1) by an integer (results.count), which will only output another integer, rounded down.

Progress is a float between 0 and 1, so you may need to cast idx or results.count to float before the division.


#3

Hi,
I’ve just looked through the code you emailed through to me. I think you’re confusing which events happen on the main thread

and which you need to dispatch to another thread.

To help anyone else here is a working solution - you’ll need to add these code snippets to a simple view app template.

I’m not saying this is how I would implement this way of working (it’s unlikely I would even given the little knowledge I have of the app) -

I’ve merely made the code work.

I added a button to a simple view app, this is the code from viewDidLoad:

UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button setTitle:@"push me" forState:UIControlStateNormal];
[button addTarget:self action:@selector(priceListSync:) forControlEvents:UIControlEventTouchUpInside];
button.frame = CGRectMake(0, 0, 300, 100);
[self.view addSubview:button];
button.center = self.view.center;

Here is the handler:

- (IBAction) priceListSync:(id) sender
{
SEssentialsProgressIndicator *progIndicator = [SEssentialsProgressIndicator
progressIndicatorOfType:SEssentialsIndicatorTypeLinearContinuous
withFrame:CGRectMake(200, 300, 400, 40)];

progIndicator.style.tintColor = [UIColor colorWithWhite:0.1 alpha:1];
[self.view addSubview:progIndicator];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[SEssentialsProgressIndicator syncWithVisual:NO progressIndicator:progIndicator];
});
}

Not how all of the UI tasks happen on the main thread which we are always on here. We then dispatch the work to be done onto a background thread.

Here is the code that actually performs the work on the dispatched thread and then signals back to the main UI thread when required to update the UI control:

+ (void)syncWithVisual:(BOOL)update progressIndicator:(SEssentialsProgressIndicator *)progressIndicator{

if (progressIndicator != nil) {

for (int i=0; i<10; i++) {

//doing work
[NSThread sleepForTimeInterval:1.0f];

dispatch_async(dispatch_get_main_queue(), ^{
progressIndicator.progress = (i+1)/10.f;
});

}

}
}

#4

tkelly

From a quick scan of your code, you appear to be dividing an integer (idx+1) by an integer (results.count), which will only output another integer, rounded down.

Progress is a float between 0 and 1, so you may need to cast idx or results.count to float before the division.

Thanks! I changed it to below and made sure I was getting the correct value this time.

float percentComplete = (idx + 1) / (float)results.count;
NSLog(@“Percent complete: %f”, percentComplete);

Wg


#5

Stu, as I noted in the ticket, that didn’t work for me but I did find out something.

When I changed my priceListSync IBAction to:

- (IBAction) priceListSync:(id) sender
{
    self.isSyncAll = NO;
   
    //Create our indicator inside the placeholder
    SEssentialsProgressIndicator *progIndicator = [SEssentialsProgressIndicator
                                                   progressIndicatorOfType:SEssentialsIndicatorTypeLinearContinuous
                                                   withFrame:CGRectMake(200, 300, 400, 40)];
    progIndicator.style.theme.primaryTintColor = [UIColor colorWithWhite:0.1 alpha:1];
    [self.view addSubview:progIndicator];

    [ShortPriceList syncWithVisual:NO progressIndicator:progIndicator];
}

and tried to call the sample code snippit in my category:

  

//Set up our callback using threads
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        /*
         * Your long-running progressive task goes here, instead of what's between the //------// blocks
         */
        //--------------------------------------------------------------------//
        for (unsigned int i = 0; i < 100; ++i) {
            [NSThread sleepForTimeInterval:0.02f];
           
            [self performSelectorOnMainThread:@selector(updateProgressByStep:) withObject:progressIndicator waitUntilDone:YES];
        }
        //--------------------------------------------------------------------//
        [self performSelectorOnMainThread:@selector(finishIndicator:) withObject:progressIndicator waitUntilDone:NO];
    });

It will not work where I need it to within the asynchronous web service call block:

[webServiceStore asynchronousFetchObjectsWithOptions:nil success: ^(NSArray *results)

If I put it outside of that, the progress indicator works exactly as expected.

Wg


#6

In another forum, I have been told that the web service call uses AFHTTPRequestOperation and that there are various issues on StackOverflow with that blocking the UI. Apparently it’s because it has a completion block on the main thread.

I had to move around the code to get it to work but I finally got it going.

EDIT NOTE: Because I am updating core data, I had to make additional changes and create a new NSManagedObjectContext and use a performBlock for my changes to take without getting core data/managed context errors. The managed objects are saved at the end in the thread context first and then merged with the main thread/parent context.

SCWebServiceStore *webServiceStore = (SCWebServiceStore *)[webServiceDef generateCompatibleDataStore];
	[webServiceStore asynchronousFetchObjectsWithOptions:nil
												 success: ^(NSArray *results)
	 {
		 NSLog(@"Successfully fetched %i objects!", results.count);
		 
		 if (results.count == 0) {
			 return;
		 }
		 
		 [NSManagedObject deleteEntity:NSStringFromClass([ServiceOrder class]) withPredicate:nil];

		 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
			 [self updateCoreData:results progressIndicator:progressIndicator];
		 });
	 }


+ (void)updateCoreData:(NSArray *)results progressIndicator:(SEssentialsProgressIndicator *)progressIndicator
{
	NSManagedObjectContext *managedContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
	[managedContext setParentContext:parentManagedObjectContext];
	
	[managedContext performBlock:^{
		[results enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
			BusinessUnit *entity = [NSEntityDescription
									insertNewObjectForEntityForName:@"BusinessUnit"
									inManagedObjectContext:managedContext];
			
			entity.entityId = [obj objectForKey:@"ID"];
			entity.entityDescription = [obj objectForKey:@"NAME"];
			entity.rowId = [obj objectForKey:@"ROWID"];
			
			dispatch_async(dispatch_get_main_queue(), ^{
				float percentComplete = (idx + 1) / (float)results.count;
				NSLog(@"Percent complete: %f", percentComplete);
				if (progressIndicator != nil) {
					progressIndicator.progress = percentComplete;
				}
				else
				{
					// Or you could simply send a notification to the view controller that has the progress indicator.
					NSDictionary *objects = [[NSDictionary alloc] initWithObjectsAndKeys:
											 [NSNumber numberWithInt:idx + 1], @"currentItem"
											 , [NSNumber numberWithInt:results.count], @"totalItems", nil];
					[[NSNotificationCenter defaultCenter] postNotificationName:@"updateSyncProgress" object:objects userInfo:nil];
				}
			});
		}];
		
		NSError *error = nil;
		// Save thread context.
		[managedContext save:&error];
		if (!error) {
			// Merge with main thread/parent context.
			[managedContext.parentContext save:&error];
			if (error) {
				NSLog(@"Error - could not save Entity on main thread context: %@", [error localizedDescription]);
			}
		}
		else {
			NSLog(@"Could not save Entity on thread context: %@", [error localizedDescription]);
		}
	}];
}

This is the code for the notification in the view controller:

- (void)viewDidLoad
{
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateProgress:) name:@"updateSyncProgress" object:nil];
}

- (void)updateProgress:(NSNotification *)notification
{
    if (notification.object != nil) {
	int currentItem = [[notification.object valueForKey:@"currentItem"] intValue];
	int totalItems = [[notification.object valueForKey:@"totalItems"] intValue];
	float percentComplete = currentItem / (float)totalItems;

	self.progressIndicator.hidden = NO;
	self.progressIndicator.progress = percentComplete;

	// Update the label placed in front of the progress indicator.
	self.progressLabel.text = [NSString stringWithFormat:@"%i of %i (%i%%)", currentItem, totalItems, (int)(percentComplete * 100)];
    }
}

Wg


#7

Thanks for sharing that with us! I’m sure it will help someone else in the future.

Jan


#8

Since I am working with core data, I ran into issues with the way I was updating it with my previous code because managed object contexts are not thread safe. I’ve updated my answer to solve that problem.

Wg