UIActivityMonitor Out of Sync - uitableview

I have reviewed many different ways to get the activity indicator to start however I feel synchronising it correctly seems a bit challenging. Nothing is broken with the below code however the Activity monitor icon(throbber) doesn't show up until the last moment before transitioning to the next view.
When the user taps the one of the elements within the UITable it kicks off getting a JSON response then takes the user to the next View. Works perfectly. Except the ActivityIndicator is late to show up as stated before.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
spinner.frame = CGRectMake(0, 0, 24, 24);
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryView = spinner;
[spinner startAnimating];
timer = [NSTimer scheduledTimerWithTimeInterval:3.0/1.0 target:self selector:#selector(loading) userInfo:nil repeats:YES];
//Go to the next view
if ([cell.textLabel.text isEqualToString:#"Vatsim Pilots"]) {
VatsimViewController *detail = [self.storyboard instantiateViewControllerWithIdentifier:#"newview"];
[self.navigationController pushViewController:detail animated:YES];
}
}
I have attempted to use the NSStimer function but clearly that doesn't work. I have seen others do this but not hitting a JSON service. I would have thought it wouldn't matter where the data is coming from and only cares when the data is ready.
Thanks in advance!

The problem is that the spinner doesn't start spinning until after the current CATransaction ends - after all your code has finished. But your code doesn't finish until the JSON response comes back, because (in code apparently not shown here) you do this interaction synchronously.
Don't. You should not be networking synchronously! You are blocking the main thread when you do that - and if you do that for too long, the WatchDog process will kill your app dead.
Thus the fact that the spinner doesn't start is actually symptomatic of something much deeper that you're doing wrong.
Having said that, the solution to your problem is to start the spinner spinning and then immediately get off the main thread, so that the CATransaction has a chance to end. The spinner will then actually start spinning! Then when your code re-enters you can start your time-consuming navigation, as in this code (from my book):
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// ... start spinner here ...
// now get off main thread
double delayInSeconds = 0.1;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// ... now do time-consuming stuff ...
// ... and finally, navigate:
UIViewController *detailViewController = [ViewController new];
[self.navigationController pushViewController:detailViewController animated:YES];
});
}

Related

Weird delay in UIPopoverController dismissal

Maybe this is purely simulator related. I have not tried it on an actual device yet.
I'm on the latest greatest MacBook with a 1TB flash drive, and 95% free processor, and less than full memory consumption.
I have a UIPopoverController with 4 items in it, sized to those items.
There's nothing complicated or multi-threaded or long running in any way associated in the UIPopoverController in question.
I've set the appear and dismiss animation at 0, yet when I tap on an item in the list, there seems to be an random indeterminate delay between 0 and .4 seconds in the popover disappearing. Of course the 0 is expected, but the times when it's nearly a half second is very noticeably longer and disconcerting.
Any idea what may be causing that?
Code that shows the popover...
-(IBAction)theLandImpsButtonPressed:(UIButton *)sender
{
iRpNameValuePopover *thePopoverContent = [[iRpNameValuePopover alloc] init];
thePopoverContent.theTableValues = [self getLandImpsChoicesList];
impsLandPopover = [[UIPopoverController alloc] initWithContentViewController:thePopoverContent];
thePopoverContent.thePopoverController = impsLandPopover;
impsLandPopover.popoverContentSize = [iRpUIHelper sizeForPopoverThatHasTitle:NO andListContent:thePopoverContent.theTableValues];
impsLandPopover.delegate = self;
[impsLandPopover presentPopoverFromRect:self.theLandImpsButton.bounds inView:self.theLandImpsButton permittedArrowDirections:UIPopoverArrowDirectionAny animated:NO];
}
Code that dismisses the popover...
BTW, there is no evaluation time incurred here [self userChoiceIsValid] because it simply returns YES right now.
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
_theChosenNameValueItem = [self.theTableValues objectAtIndex:indexPath.row];
[self acceptUserChoiceAndClose];
}
// This contentViewController is encapsulated INSIDE a UIPopoverViewController, and this class cannot itself
// close the popover which contains it, hence the need for the reference to the popover controller
// It is the popover's delegate... the one that created the popover, that is able to close it.
-(void)acceptUserChoiceAndClose
{
_theUserChoseAValue = NO; // Start by assuming they didn't chose a valid value.
if ([self userChoiceIsValid])
{
// Set variable that indicates the user chose a value which can be saved to core data, and/or presented on screen.
_theUserChoseAValue = YES;
// Close the popover.
[_thePopoverController dismissPopoverAnimated:NO];
// Notify the class that presented the popover that the popover has been dismissed.
// It will still be available to the dismissal method where code can retrieve the user's choice, and set the popover to nil.
if (_thePopoverController.delegate && [_thePopoverController.delegate respondsToSelector:#selector(popoverControllerDidDismissPopover:)])
{
[_thePopoverController.delegate popoverControllerDidDismissPopover:_thePopoverController];
}
}
else
{
[self showValidationFailureMessageToUser];
}
}
Dismissing the viewController in main thread will solve the issue.
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:nil];
});
I would check it out in the profiler and see what the time is being spent on.
There's a good tutorial here.
UIPopoverPresentationController *popOverView;
//////
Dismiss it...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
[popOverView.presentedViewController dismissViewControllerAnimated:NO completion:nil];
popOverView = nil;
});
});

UIActivityIndicatorView not staring

I am trying to set a 'UIActivityIndicatorView' as an accessoryView in one of my 'UITableViewCell', in a such way as, the ActivityIndicator start animating when the user touch this cell. I add the following code at 'didSelectRowAtIndexPath' method:
UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath];
UIActivityIndicatorView * activity = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
NSThread * newThread1 = [[NSThread alloc]initWithTarget:self selector:#selector(carregarNoticias) object:nil];
switch (indexPath.row) {
case 0:
cell.accessoryView = activity;
[activity startAnimating];
sleep(0.01);
[newThread1 start];
while (![newThread1 isFinished]) {
//waiting for thread
}
[activity stopAnimating];
[self.navigationController pushViewController:noticias animated:YES];
break;
I was expecting 'UIActivityIndicatorView' animating while the newthread1 is running. However, the animating only start when the newThread1 finish (and therefore I don't want the animation anymore). I added 0.01 seconds as sleep time to give time to the animation start. Nevertheless, this also did't solve.
Does anyone know what my error is? I appreciate any help!
I dont really know what you are trying to do adding delay like that.. try this line below, might be the one your look for.. and another thing.. please dont block the main thread like sleep(0.01); that, users wont like it..
cell.accessoryView = activity;
[activity startAnimating];
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
// the system will wait until this
dispatch_async(dispatch_get_main_queue(), ^(void){
[newThread1 start];
});
// is finished, maybe you dont need the `[NSThread sleepForTimeInterval:3];`
// pick the one that suits your implementation..
[NSThread sleepForTimeInterval:3];
dispatch_async(dispatch_get_main_queue(), ^(void){
[activity stopAnimating];
[self.navigationController pushViewController:noticias animated:YES];
});
});
[NSThread sleepForTimeInterval:3] sleep the thread like the one you have sleep(0.01);
But using it inside
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){ });
makes it async. and wont block the main thread..
while the dispatch_async(dispatch_get_main_queue(), ^(void){ fires your code at the main thread..
Happy coding, cheers! :)

Value not being set the second time on iOS 6. Threading issue?

I'm writing an iOS application that populates an array using the data retrieved from the server and displays it in a picker view.
Everything goes smoothly the first time the view is displayed; However, when switching to another view which uses a camera to scan stuff and switch back using a slide-out menu (built using SWRevealViewController), it fails to populate the array. When we access it on the UI thread after the background task has finished to retrieve the records, an NSRangeException is thrown with the error index 0 beyond bounds for empty array.
I'm pretty sure the background task is being run and the data is being retrieved successfully as I log every single request to the server.
I believe it might be an issue with concurrency and the background thread not updating the variable.
As far as we have tested, this issue is only present on iOS 6, and does not happen, or at least has not yet, on iOS 7.
This is the code used to retrieve and set the array:
dispatch_queue_t queue = dispatch_queue_create("com.aaa.bbb", NULL);
dispatch_async(queue, ^{
_events = [_wi getEvents:_auth_token];
dispatch_async(dispatch_get_main_queue(), ^{
// code to be executed on the main thread when background task is finished
[_mPicker reloadComponent:0];
// Set the default on first row
[self pickerView:_mPicker didSelectRow:0 inComponent:0];
[pDialog dismissWithClickedButtonIndex:0 animated:YES];
});
});
This is the prepareforSegue method in my SidebarViewController that is responsible for switching between views when an item is selected from the slide-out menu.
- (void) prepareForSegue: (UIStoryboardSegue *) segue sender: (id) sender
{
// Set the title of navigation bar by using the menu items
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
UINavigationController *destViewController = (UINavigationController*)segue.destinationViewController;
destViewController.title = [[_menuItems objectAtIndex:indexPath.row] capitalizedString];
if ( [segue isKindOfClass: [SWRevealViewControllerSegue class]] ) {
SWRevealViewControllerSegue *swSegue = (SWRevealViewControllerSegue*) segue;
swSegue.performBlock = ^(SWRevealViewControllerSegue* rvc_segue, UIViewController* svc, UIViewController* dvc) {
UINavigationController* navController = (UINavigationController*)self.revealViewController.frontViewController;
[navController setViewControllers: #[dvc] animated: NO ];
[self.revealViewController setFrontViewPosition: FrontViewPositionLeft animated: YES];
};
}
}
The views are linked together from the storyboard for switching.
The error occurs when I try to retrieve a specific entry from my events array in the pickerView:didSelectRow:inComponent: method :
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
// Set the current id
_currentId = [NSString stringWithFormat:#"%d", row];
_currentName = [[_events objectAtIndex:row] objectForKey:#"event_name"];
-----------^
...
Here's the list of running threads and the call stack.
From my experience in Android, I think that this might have to do something with me not finishing the background task properly the first time, or somehow the code that is supposed to be run after the background task, is run alongside it the second time.
I would appreciate any suggestions that might help me with this issue!
Thanks
The variable _events is accessed from different threads without any synchronization mechanism.
Try this:
dispatch_queue_t queue = dispatch_queue_create("com.aaa.bbb", NULL);
dispatch_async(queue, ^{
NSArray* events = [_wi getEvents:_auth_token];
dispatch_async(dispatch_get_main_queue(), ^{
_events = [events copy];
// code to be executed on the main thread when background task is finished
[_mPicker reloadComponent:0];
// Set the default on first row
[self pickerView:_mPicker didSelectRow:0 inComponent:0];
[pDialog dismissWithClickedButtonIndex:0 animated:YES];
});
});
Also _events should be an NSArray, not NSMutableArray, to avoid such problems.

EGOTableViewPullRefresh Too quick

I've put EGOTableViewPullRefresh in my project. However, I've noticed one troubling thing. If I ever need to download a (semi-moderately) large file by pulling down to refresh, EGOTableViewPullRefresh says that I am done downloading much faster than I really am.
Is there a way to make EGOTableViewPullRefresh show that is is still downloading. In other words, I would like the words "loading" to last longer on the screen when I pull down to refresh. Actually, I would like "loading" to be on the screen until it is done loading.
In viewDidloadmethod:
if (_refreshHeaderView == nil) {
EGORefreshTableHeaderView *view = [[EGORefreshTableHeaderView alloc] initWithFrame:CGRectMake(0.0f, 0.0f - self.tableView.bounds.size.height, self.view.frame.size.width, self.tableView.bounds.size.height)];
view.delegate = self;
[self.tableView addSubview:view];
_refreshHeaderView = view;
}
// update the last update date
[_refreshHeaderView refreshLastUpdatedDate];
You should set pullTableIsRefreshing to NO in block
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//your data fetching code
dispatch_async(dispatch_get_main_queue(), ^{
//your UI code
self.tableView.pullTableIsRefreshing = NO;
});
});

AddSubview in didSelectRowAtIndexPath

I have one problem.
I have table view and when I click on cell I load data from server. Because this could take some time I want to show activity indicator view.
-(void)startSpiner{
CGRect screenRect = [[UIScreen mainScreen] bounds];
UIView * background = [[UIView alloc]initWithFrame:screenRect];
background.backgroundColor = [UIColor blackColor];
background.alpha = 0.7;
background.tag = 1000;
UIActivityIndicatorView * spiner = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
spiner.frame = CGRectMake(0, 0, 50, 50);
[spiner setCenter:background.center];
[spiner startAnimating];
[background addSubview:spiner];
[background setNeedsDisplay];
[self.view addSubview:background];
}
This works fine, but when I put this in
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[self startSpiner];
Server *server = [[Server alloc]init];
self.allItems = [server getDataGLN:self.object.gln type:1];
}
I see that UIActivityIndicatorView is shown after it get data from server.
How to force main view to update immediately?
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[self startSpiner];
////If your server object is performing some network handling task. dispatch a
////background task.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
Server *server = [[Server alloc]init];
self.allItems = [server getDataGLN:self.object.gln type:1];
});
}
The UI normally isn't updated until your code returns control to the run loop. Your getDataGLN:type: method isn't returning until it gets the data from the server. Thus the UI cannot be updated until you've got the data from the server.
Don't do that. Load your data on a background thread and return control of the main thread to the run loop immediately. You will find lots of help in the Concurrency Programming Guide and in Apple's developer videos.
Why do you call setNeedsDisplay?
Anyway, The right way to do it, is not creating all the UI when you want to show loading indicator.
Do it in advance - in ViewDidLoad for example, and just hide the background view.
When you want to show it, just turn it's hidden property to NO.

Resources