I've been trying for about a day to get a table cell to display an activity indicator while it loads a new view. Upon didSelectRowAtIndexPath I'd like to display the indicator while the following runs
[self.navigationController pushViewController:subKeywordController animated:YES];
This controller then runs a fairly intensive SQL query
I've searched the web and read dozens of posts but none seem to help with my specific problem. I understand that I need to run the indicator in another thread because the push and subsequent load takes precedence but I'm unsure how to do it. I thought about running the SQL query in the controller before but that's getting very messy.
The odd thing is that I've been using MBProgressHUD to display busy cursors in this same table view with no issues. It's only when I apply a search and then select one of the results that causes this error:
bool _WebTryThreadLock(bool), 0x1d79b0: 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...
The app continues on the iPhone but crashes the simulator.
Any help would be greatly appreciated.
The problem is that your task in the controller is holding up the UI code (but you probably already know that!). A cheap and easy way of solving this is to put a tiny delay on starting your slow task (in this case your SQL query) using a timer :
- (void)viewDidLoad {
[super viewDidLoad];
// instead of running the SQL here, run it in a little bit
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(doSQL:) userInfo:nil repeats:NO];
// Show the please wait stuff
[activityIndicator setHidden:NO];
}
- (void)doSQL:(NSTimer *)timer {
// Do your sql here
}
The other way of solving this is to move your SQL into a seperate thread :
- (void)viewDidLoad {
[super viewDidLoad];
// instead of running the SQL here, run it in a little bit
[self performSelectorInBackground:#selector(doSQL) withObject:nil];
// Show the please wait stuff
[activityIndicator setHidden:NO];
}
- (void)doSQL {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Do your sql here
// Tell the main thread
[self performSelectorOnMainThread:#selector(doneSQL) userInfo:nil waitUntilDone:YES];
// Cleanup
[pool release];
}
- (void)doneSQL {
// Update your UI here
}
Hope that helps!
Related
I've been researching this for a few days, but nothing I've found works.
Here's the desired process: User presses the enter button on a pop-up window -> ActivityIdicator appears -> saving process occurs -> ActivityIndicator disappears.
However, for some reason the ActivityIndicator does not show up until after the process is complete, rendering it completely useless.
I attempted to follow the process described here: UIActivityIndicator not working properly?
Here is the code for the pop-up window
-(void)alertView:(UIAlertView *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
// the user clicked one of the Enter/Cancel buttons
[self performSelector:#selector(DisplaySpinner) withObject:nil afterDelay:0];
if (buttonIndex == 1)
{
[self performSelector:#selector(EnterButtonClicked) withObject:nil afterDelay:0];
}
else
{
NSLog(#"Name Cancel Clicked");
}
[NameField resignFirstResponder];
}
Here is the code for the DisplaySpinner method:
-(void)DisplaySpinner{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
[self.view addSubview:loadingIndicator];
[loadingIndicator startAnimating];
[pool release];
}
The EnterButtonClicked Method contains the saving process. Despite running in seperate processes, The ActivityIndicator doesn't show up until after the process is complete.
Any suggestions?
The app doesn't update the screen to show the UIActivityIndicatorView until the main run loop regains control. If your processing task blocks the main thread, the no UI updates will take place until it is finished. You should do your processing asynchronously.
When a rotation event happens, the willRotate... and willAnimateRotation... methods are called in one pass through the main run loop. So you block on the method before displaying the activity indicator.
To make this work, you need to push the method task over to another thread. That method would call back to this view controller when the work is completed so the view can be updated. I would put show the activity indicator in the willAnimateRotation... method.
What you need to do is forcing the UIActivityIndicatorView to start displaying even though the run loop won't be ended. One way is -
[self performSelector:#selector(animation) withObject:nil afterDelay:0]
-(void)startSpinner
{
NSAutoreleasepool *pool = [[NSAutorepleasepool alloc]init];
[indicatorView startAnimating];
[pool release];
}
So essentially, performSelector sets up a timer to perform the animation message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.
Please note that specifying a delay of 0 does not necessarily cause the selector to be performed immediately. The selector is still queued on the thread’s run loop and performed as soon as possible.
I had the UIActivityIndicatorView working fine in simulator and other 3.0 devices in my app. But I found out that it was not spinning (or showing) in the new iphone 4. Basically I need to show the activity indicator when a button is clicked and hide it when the button click event is complete. I was using the approach below.
[NSThread detachNewThreadSelector: #selector(spinBegin) toTarget:self withObject:nil];
from this link. As mentioned, it correctly spins the activity indicator on all except 4.*.. not sure why. To get around this, I also followed another approach something like (from developer.apple.com)
`
(IBAction)syncOnThreadAction:(id)sender
{
[self willStartJob];
[self performSelectorInBackground:
#selector(inThreadStartDoJob:)
withObject:theJobToDo
];
}
(void)inThreadStartDoJob:(id)theJobToDo
{
NSAutoreleasePool * pool;
NSString * status;
pool = [[NSAutoreleasePool alloc] init];
assert(pool != nil);
status = [... do long running job specified by theJobToDo ...]
[self performSelectorOnMainThread:
#selector(didStopJobWithStatus:)
withObject:status
waitUntilDone:NO
];
[pool drain];
}
`
The problem with this was that, it is showing the acitivityVIewIndicator spinning correctly (at least on the simulator) but after it stops, the built in activity indicator in the top bar (where it shows the battery% etc) is still spinning.
I'm new to objective C. I have finished my app completely but for this silly thing. I realize there is no way to display UIActivityView without starting another thread. and finally, just to rant, I don't understand why they have to make it so complicated. I mean they knew it was going to have this problem, why not provide a sample code everyone can use rather than deriving their own solutions.
Finally, can anyone please provide me with a direction or some sample code. I would really appreciate it. I have been searching for a few hours now and have not found anything really that works!
Why are you starting/stopping the indicator on a separate thread? Any methods you send to your UIActivityIndicatorView must be sent on the main (UI) thread.
Any events sent by a button pressed will automatically be run on the main thread. If you're using background threads to complete the process, you could do something like:
- (IBAction)buttonPressed:(id)sender {
// This runs on the main thread
[activityIndicator startAnimating];
[self performSelectorInBackground:#selector(inThreadStartDoJob:) withObject:theJobToDo];
}
- (void)inThreadStartDoJob:(id)theJobToDo {
// Set up autorelease pool
...
// Run your long-running action
...
// Stop the spinner. Since we're in a background thread,
// we need to push this to the UI Thread
[activityIndicator performSelectorOnMainThread:#selector(stopAnimating) withObject:nil waitUntilDone:YES];
}
Edit: As for the activity indicator in the top bar (where the battery is), doesn't this automatically start/stop based on network activity?
I have implemented a subview which is supposed to load immediately when I click a button in the parent view. After loading the subview(which is basically holding an activityindicator), the program is supposed to process a method(which gets data from a server, so takes time) in it.
However, I am unable to do it.
What happens now is, when I click the button on the parent view, it processes the method first and only after that does the subview load on screen.
Why is this so? Is there any specific functions I could use to make my method load only after the view has loaded?
I have faced the same problem and i have used NSAutoreleasePool and solved this problem. I hope it will help you.
- (void)viewDidLoad {
[self performSelectorInBackground:#selector(loadXml) withObject:nil];
}
-(void) loadXml{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSString * path =#"http:www.YOUR_RSS_FEED.com";
[self parseXMLFileAtURL:path]; //(Instead of ViewDidAppear)
[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
[pool drain];
}
Thanks.
A few different approaches:
If the subview has functionality as loading an image and doing more complicated stuff than just being a view, a good approach is to make it into a ViewController instead.
In the top ViewController use:
- (void) loadView {
[super loadView];
}
to set up things that need to be ready upon displaying the view.
The use:
-(void) viewDidLoad {
}
to do additional setup.
In your case it sounds as if you need to add the indicator in the loadView method, then start the retrieval of data in the viewDidLoad method.
If you are accessing web services etc. always do this on a different thread. (look into NSOperation for a good, simple way of achieving this).
I have a question that we might answer together i have a tickertape in my iphone app (Like those stick tickers) and i use a NSThread to keep the memory away from the main thread so it will not slow down the app. Now the thing is it does its job well but when i scroll on a UITableView that i have on the same view i notice that my ticker tape animation stops to work.
ViewController.m (Main view of this object has the ticker tape on it)
-(void)startTicker {
[NSThread detachNewThreadSelector:#selector(start) toTarget:ticker withObject:nil];
}
TickerView.c (This handles the tickertape animation)
// Called from the viewcontroller
-(void) start {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self performSelectorOnMainThread:#selector(loop) withObject:nil waitUntilDone:YES];
[pool release];
}
-(void)loop {
timerHandle = [NSTimer scheduledTimerWithTimeInterval:.01f target:self selector:#selector(render) userInfo:nil repeats:YES];
}
-(void) render {
// Does a *** load of calculations here and moves the items in the tickertape..
}
My Question: How can i prevent the UITableview or any other view / touch event to block this thread from updating the tickertape animation ?.
Your NSTimer is not running on a background thread, but on the main thread. It will block anytime something else runs on the main thread. -performSelectorOnMainThread: means that anything done within the method called will run on the main thread.
To make your loop truly independent of the main thread, you could set up a while loop within your start method that sleeps for a given interval on every pass, then calls your render method. You'd need to make sure that all user interface updates within your render method get performed on the main thread, but make waitUntilDone NO for those method calls. I've also done this using NSOperations, where as one operation finishes I add another to the queue.
Also, running this render operation 100 times per second is excessive. I'd back that down a bit, or even better, look at using Core Animation for your ticker to make your drawing more efficient.
I have got a memory bug that seems to boil down to something happening in a thread. I am having difficulties troubleshooting this.
I have a UIViewController, that when active, i.e. the user is using its view, retrieves updates from a web service in an NSThread.
This is done every 3 minutes and this delay is controlled by a:
[self performSelector:#selector(timerDone) withObject:nil afterDelay:180.0];
The timerDone method now starts the NSThread that retrieves the web service data and also it sends the performSelector message again. This is a little "check for updates, populate views, shut everything down, repeat" routine that works just fine.
Now, the user can of course suddenly tap a button an load up a second UIViewController. When this happens I call:
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(timerDone) object:nil];
And do my cleaning up in the dealloc method.
My question is now: What happens if the NSThread was running while the user changed the view and set in motion the deconstruction of this object that is the starting point of the NSThread?
Should I keep a BOOL around that tells me if the NSThread is still active, and if so, what to do with the NSThread if this is the case.
The threading is done like this:
- (void) runTimer {
[self performSelector:#selector(timerDone) withObject:nil afterDelay:180];
}
- (void) timerDone {
[self performSelector:#selector(runTimer) withObject:nil afterDelay:2];
[NSThread detachNewThreadSelector:#selector(updateAllVisibleElements) toTarget:self withObject:nil];
}
- (void) updateAllVisibleElements {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//call approiate web service
[pool release];
}
You have two problems here: first, you're using performSelector:withObject:afterDelay: to do what an NSTimer does best (periodic callback). cancelPreviousPerformRequestsWithTarget:selector:object: can be quite expensive, and because of your threading is likely creating race conditions.
Second problem: each thread has its own run loop, and both mechanisms (performSelector:... and NSTimer) and are tied to the current thread's run loop.
Here's what I recommend: Create a single, long-lived NSThread with its own explicit run loop for all your update needs. Look at the Threading Programming Guide for some good example code of this. On that thread, set up a 3-minute repeating NSTimer. Every 3 minutes, update.
If you need to schedule an update outside the three-minute cycle, then you use performSelector:onThread:withObject:waitUntilDone: to call your updateAllVisibileElements. The way I generally do this is to encapsulate all of the thread logic into a single object (WebServiceController or whatever). It creates it own NSThread and saves it in an ivar. Then I use code like this:
- (void)requestUpdate
{
if ([NSThread currentThread] != self.thread)
{
[self performSelector:#selector(update) onThread:self.thread withObject:nil waitUntilDone:NO];
return;
}
else
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//call approiate web service
[pool drain];
}
}
One more note: you mention that the background thread "populates views." A background thread should never call into UIKit. UIKit is not thread safe and should only be called on the main thread. I typically achieve this by posting notifications onto the main thread which the view controllers observe. The "updating" object should not know anything about the UI. That breaks the Model-View-Controller paradigm of Cocoa.