I have an App using UITableViews and fetching data from a server. I am attempting to put a UIActivityIndicatorView on the Parent UITableView, so it spins while the Child UITableView is loading. I have the UIActivityIndicatorView all hookedup through Interface Builder, etc.
-(void)spinTheSpinner {
NSLog(#"Spin The Spinner");
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[spinner startAnimating];
[NSThread sleepForTimeInterval:9];
[self performSelectorOnMainThread:#selector(doneSpinning) withObject:nil waitUntilDone:NO];
[pool release];
}
-(void)doneSpinning {
NSLog(#"done spinning");
[spinner stopAnimating];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[NSThread detachNewThreadSelector:#selector(spinTheSpinner) toTarget:self withObject:nil];
NSMutableString *tempText = [[NSMutableString alloc] initWithString:[categoryNumber objectAtIndex:indexPath.row]];
Threads *viewController = [[Threads alloc] initWithNibName:#"newTable" bundle:nil tagval:tempText SessionID:PHPSESSID];
[self.navigationController pushViewController:viewController animated:YES];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[viewController release];
}
So, when I push the Child UITableView onto the screen, the UIActivityIndicatorView (spinner) just sits there on the Parent UITableView. If I go to the Child UITableView and then quickly go back to the Parent View, I can catch the UIActivitIndicatorView in the act of spinning. The NSLogs are showing at the correct times - when I first tap the cell in the Parent UITableView, the "Spin The Spinner" appears in the Log. 9 seconds later, I get "done spinning" in the Log, but the spinner never spins unless I pop back to the Parent UITableView in time.
Ideas of why this is not working properly?
Perhaps the sleepForTimeInterval is blocking the animation.
Try removing the sleepForTimeInterval and replace performSelectorOnMainThread with a call to performSelector:withObject:afterDelay:.
let the ui display it outside thread:
[NSObject dispatchUI:^{
[spinner startAnimating];
}];
Related
I want to build object and then open a controller with it. Building can take up to 5 seconds and I want to show a message while it processing.
I have the following implementation of didSelectRowAtIndexPath:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
messageView.hidden = NO;
// Some methods
Controller *ctrl = [Controller new];
[self.navigationController pushViewController:ctrl animated:YES];
}
Everything is good but there is a problem: messageView appears ONLY when push animation starts. What can I do to fix that?
Similar to Jonathan's answer, delay the push a little to give the messageView time to appear.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
messageView.hidden = NO;
int64_t oneMillisecond = NSEC_PER_MSEC;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, oneMillisecond), dispatch_get_main_queue(), ^(void){
// Some methods
Controller *ctrl = [Controller new];
[self.navigationController pushViewController:ctrl animated:YES];
});
}
It's not displaying because you're blocking the main thread while building the object.
The user interface will not update until you return control to the run loop.
The solution is to build your object on a background thread, the easiest way of doing this is with libdispatch, like so:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
messageView.hidden = NO;
// you may want to disable user interaction while background operations happen
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
// Perform your lengthy operations here
Controller *ctrl = [[Controller alloc] init];
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController pushViewController:ctrl animated:YES];
}
});
}
If you want a UIAlertView you can use this code:
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Title..." message:#"More?" delegate:nil cancelButtonTitle:nil otherButtonTitles:nil];
[alert show];
After your done you can call this to dismiss:
[alert dismissWithClickedButtonIndex:0 animated:YES];
Can you try with this:
[UIView animateWithDuration:0.5f delay:0.0f options:UIViewAnimationCurveLinear animations:^(void)
{
messageView.hidden = NO;
}
completion:^(BOOL finished)
{
Controller *ctrl = [Controller new];
[self.navigationController pushViewController:ctrl animated:YES];
}];
The view probably isn't redrawn during your didSelectRowAtIndexPath call.
So... I would try running the long running method in a block. Then block the main thread with you messageView animation and have your block post a notification or something to shut it down.
You might want to have some kind of condition for the messageView to shut itself down after a certain time.
I've added the ELCimagepicker (https://github.com/Fingertips/ELCImagePickerController) to my project and it works perfectly, allowing the user to select multiple images for a slideshow. But when you click 'Save', there can be a lengthy delay depending on how many photos were added.
I've been trying to add a UIActivityIndicator when the user clicks 'Save', but having trouble due to the modal view that is presented. I can call a method from the activity that ELCimagepicker presents (ELCImagePickerController) and this gets actioned by the activity handling the presenting of the image picker. But whenever I try to add to the view, it isn't shown as the modal is on top of the activity indicator.
I've tried using bringSubviewToFront, I've tried adding the code directly to the imagepicker method file with [[self parentViewController] addSubView], but no luck.
Here's the latest code I tried: (indicator is declared in the .h file as UIActivityIndicator *indicator)
indicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
indicator.hidden=false;
[self.navigationController.view addSubview:self.indicator];
[self.navigationController.view bringSubviewToFront:self.indicator];
[indicator startAnimating];
if([delegate respondsToSelector:#selector(elcImagePickerController:showIndicator:)]) {
[delegate performSelector:#selector(elcImagePickerController:showIndicator:) withObject:self withObject:#"test"];
}
Has anyone had any success with adding a UIActivityIndicator on top of the ELCimagepicker, or another modal view handled by another class?
I've tried MBProgressHUD but couldn't get that working quite right either - it would show up when I used it in the ELCimagepicker class, but crashed on removal with:
bool _WebTryThreadLock(bool), 0x42368e0: Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread. Crashing now...
Any help would be fantastic.
Thanks.
I have figure out your problem. You can do this as below..
-(void)selectedAssets:(NSArray*)_assets {
UIActivityIndicatorView * activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
UIViewController * topView = [self.viewControllers objectAtIndex:[self.viewControllers count]-1];
activityIndicator.center = CGPointMake(topView.view.frame.size.width/2, topView.view.frame.size.height/2);
[activityIndicator setHidden:NO];
[topView.view addSubview:activityIndicator];
[topView.view bringSubviewToFront:activityIndicator];
[activityIndicator startAnimating];
[self performSelector:#selector(doProcess:) withObject:_assets afterDelay:2.1];
}
- (void) doProcess:(NSArray *)_assets {
NSMutableArray *returnArray = [[[NSMutableArray alloc] init] autorelease];
for(ALAsset *asset in _assets) {
NSMutableDictionary *workingDictionary = [[NSMutableDictionary alloc] init];
[workingDictionary setObject:[asset valueForProperty:ALAssetPropertyType] forKey:#"UIImagePickerControllerMediaType"];
[workingDictionary setObject:[UIImage imageWithCGImage:[[asset defaultRepresentation] fullScreenImage]] forKey:#"UIImagePickerControllerOriginalImage"];
[workingDictionary setObject:[[asset valueForProperty:ALAssetPropertyURLs] valueForKey:[[[asset valueForProperty:ALAssetPropertyURLs] allKeys] objectAtIndex:0]] forKey:#"UIImagePickerControllerReferenceURL"];
[returnArray addObject:workingDictionary];
[workingDictionary release];
}
[self popToRootViewControllerAnimated:NO];
[[self parentViewController] dismissModalViewControllerAnimated:YES];
if([delegate respondsToSelector:#selector(elcImagePickerController:didFinishPickingMediaWithInfo:)]) {
[delegate performSelector:#selector(elcImagePickerController:didFinishPickingMediaWithInfo:) withObject:self withObject:[NSArray arrayWithArray:returnArray]];
}
}
Let me know if this answer help you ...
Thanks,
MinuMaster
It looks like you are updating UI on a background thread. All UIKit updates are to be done in the main thread. So I recommend you execute methods doing UI updates using performSelectorOnMainThread:withObject:.
I solved the issue like this
activityIndicatorObject = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
// Set Center Position for ActivityIndicator
activityIndicatorObject.center = CGPointMake(150, 250);
activityIndicatorObject.backgroundColor=[UIColor grayColor];
// Add ActivityIndicator to your view
[self.view addSubview:activityIndicatorObject];
activityIndicatorObject.hidesWhenStopped=NO;
[activityIndicatorObject startAnimating];
I want to display the view first and then load the data in a background thread. When I navigate from root controller to the view controller, I want to display the view first. As of now, it stays on the root controller until the view controller is loaded. Here's my code for the root controller.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
ProductDetailViewController *tempProductDetail = [[ProductDetailViewController alloc] init];
[self.navigationController pushViewController:tempProductDetail animated:YES];
[tempProductDetail release];
}
ProductDetailViewController, here I want to display the view first and then load the data...
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:YES];
[self performSelectorOnMainThread:#selector(workerThread) withObject:nil waitUntilDone:NO];
}
-(void) workerThread{
NSAutoreleasePool *arPool = [[NSAutoreleasePool alloc] init];
[arPool release];
}
Don't know what I'm doing wrong. Please, help.
Use [self performSelectorInBackground:#selector(workerThread) withObject:nil]; instead of
[self performSelectorOnMainThread:#selector(workerThread) withObject:nil waitUntilDone:NO];
found the answer for this issue,
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:YES];
[self performSelectorInBackground:#selector(workerThread) withObject:nil];
}
- (void) workerThread
{
// Set up a pool for the background task.
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// only do data fetching here, in my case from a webservice.
//...
// Always update the components back on the main UI thread.
[self performSelectorOnMainThread:#selector(displayView) withObject:nil waitUntilDone:YES];
[pool release];
}
// Called once the background thread task has finished.
- (void) displayView
{
//here in this method load all the UI components
}
Consider using the following pattern instead for threading, in my opinion it's much cleaner:
- (void)viewWillAppear:(BOOL)animated
{
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:self
selector:#selector(someFunction)
object:nil];
[[NSOperationQueue currentQueue] addObject:operation]; // this will actually start the thread
[operation release];
}
- (void)someFunction
{
// don't need to initialize and release an autorelease pool here,
// you can just write a function as usual ...
[self updateUI];
}
- (void)updateUI
{
if (![NSThread isMainThread]) // if we need a UI update, force it on main thread
{
[self performSelectorOnMainThread:#selector(updateUI) withObject:nil waitUntilDone:YES];
return;
}
// do UI updates here
}
By writing code in this way, you can more dynamically decide which function you want to thread, since there is no autorelease pool requirement. If you need to do UI updates, the updateUI function will make sure for itself that it's running on the main thread, so the caller doesn't need to take this into account.
I have following error msg in console when using NSThread
"Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread. Crashing now..."
I have submit my sample code here
- (void)viewDidLoad {
appDeleg = (NewAshley_MedisonAppDelegate *)[[UIApplication sharedApplication] delegate];
[[self tblView1] setRowHeight:80.0];
[super viewDidLoad];
self.title = #"Under Ground";
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[NSThread detachNewThreadSelector:#selector(CallParser) toTarget:self withObject:nil];
}
-(void)CallParser {
Parsing *parsing = [[Parsing alloc] init];
[parsing DownloadAndParseUnderground];
[parsing release];
[self Update_View];
//[myIndicator stopAnimating];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}
here "DownloadAndParseUnderground" is the method of downloding data from the rss feed and
-(void) Update_View{
[self.tblView1 reloadData];
}
when Update_View method is called the tableView reload Data and in the cellForRowAtIndexPath create error and not display custom cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
CustomTableviewCell *cell = (CustomTableviewCell *) [tblView1 dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:#"customCell"
owner:self
options:nil];
cell = objCustCell;
objCustCell = nil;
}
if there is a crash, there is a backtrace. Please post it.
Method names start with lowercase letters, are camelCased, and do not contain underscores. Following these conventions will make your code easier to read by other iOS programmers and learning these conventions will make it easier to understand other iOS programmer's code.
You can't directly or indirectly invoke methods in the main thread from background threads. Your crash and your code both indicate that you are freely interacting with the main thread form non-main threads.
The documentation on the use of threads in iOS applications is quite extensive.
Your problem should come because you load your UIViewController from a thread that's not the main thread. Tipically when you try to charge data before loading the view.
To arrange this you can try to do this
1. Add a method to load your viewcontroller with just one param
-(void)pushViewController:(UIViewController*)theViewController{
[self.navigationController pushViewController:theViewController animated:YES];}
2.Change your code (commented below) in your asynchronous loading to "PerformSelectorOnMainThread"
-(void)asyncLoadMyViewController
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
MyViewController *myVC = [[myVC alloc] initWithNibName:#"myVC" bundle:nil ];
[self performSelectorOnMainThread:#selector(pushViewController:) withObject:myVC waitUntilDone:YES];
// [self.navigationController pushViewController:wnVC animated:YES];
[wnVC release];
[pool release];
}
ok please explain proper Why you require thread in parsing method? in your code u use table reload method in properly in thread....
because
u cant put any thing which relavent to your VIEW in thread...
u can put only background process like parsing in it... if u want reload table after parsing u can use some flag value in your code and after parsing u load table
Try change CallParser method to
-(void)CallParser {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Parsing *parsing = [[Parsing alloc] init];
[parsing DownloadAndParseUnderground];
[self performSelectorOnMainThread:#selector(Update_View)
withObject:nil
waitUntilDone:NO];
[parsing release];
[pool release];
}
And move
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
line to Update_View method
You can't access any UI elements from a background thread. You certainly can't create views on a background thread. Use "performSelectorOnMainThread" method instead of "detachNewThreadSelector" method.
All the best.
My issue is that the back button will not restore its visibility if my web request does not finish before or soon after ViewWillAppear has fired.
I have a navigation based iPhone 4.0 application used a simple Root and Detail view setup.
I am working with data that is returned from a webservice so when I push my detail view in its ViewDidLoad function I call my web service method in a separate thread and the Iphone lifecycle does its thing on the main thread. I must disable/hide the back button until the web request has finished (or failed) so I call self.navigationItem.hidesBackButton = YES; in ViewDidLoad and self.navigationItem.hidesBackButton = NO; in the delegate function which fires once my web request has finished or failed.
I already tried the following:
[self.navigationItem performSelectorOnMainThread:#selector(setHidesBackButton:) withObject:NO waitUntilDone:NO];
[self.navigationItem setHidesBackButton:NO];
[self.view setNeedsDisplay];
[self.navigationController.view setNeedsDisplay];
UINavigationItem *nav = self.navigationItem;
nav.hidesBackButton = NO;
Root View Controller Push Code:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
ArticleViewController *articleViewController = [[ArticleViewController alloc] initWithNibName:#"ArticleViewController" bundle:nil];
NewsArticle *newsArticle = [newsItems objectAtIndex:indexPath.row];
articleViewController.articleID = newsArticle.newsID;
[self.navigationController pushViewController:articleViewController animated:YES];
[newsArticle release];
[articleViewController release];
}
Details View Controller Code:
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.hidesBackButton = YES;
id scrollView = [[[self webContent] subviews] objectAtIndex:0];
if([scrollView respondsToSelector:#selector(setBackgroundColor:)] )
{
[scrollView performSelector:#selector(setBackgroundColor:)
withObject:[UIColor blackColor]];
}
[self getNewsArticle];
}
//Fires when the web request has finished
- (void) finish:(NewsArticle *)newsArticleFromSvc {
self.navigationItem.hidesBackButton = NO;
self.newsArticle = newsArticleFromSvc;
[self bindNewsArtice];
}
Any help is GREATLY appreciated I can hardly ##$&^ believe that hiding a button in a UI could cause me this much wasted time.
Try use this method of UINavigationItem :
- (void)setHidesBackButton:(BOOL)hidesBackButton animated:(BOOL)animated
I wasn't able to solve this problem. Instead I tweaked my App Logic to make hiding he back button not necessary.