I am using MBProgressHUD to display a "busy" animation to use user while a UITableView is being populated. The UITableView blocks the main thread so the animation does not even appear until the table finishes loading.
Is there a way to make the busy animation run on another thread while the UITableView occupies the main thread?
UIKit does its drawing when the run loop completes the current cycle. In other words, if you're configuring a view (e.g., MBProgressHUD), the changes won't be visible until the next run loop iteration. Thus if you don't allow the run loop to spin by blocking the main thread, the UI changes won't appear immediately.
If you can't do your work on a background thread, you need to allow the run loop to complete its cycle before you start your long-running blocking task on the main thread.
You can do this by scheduling execution on the next run loop iteration.
// Setup and show HUD here
[self performSelector:#selector(myTask) withObject:nil afterDelay:0.001];
Hide the HUD at the end of myTask.
Or you can run the run loop manually.
// Setup and show HUD here
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantPast]];
// Insert myTask code here
// Hide the shown HUD here
Or (if you can use blocks)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.001 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void){
// Insert myTask code here
});
Related
I am trying to understand the run loop. Which is the best way to code this?
for (UIView *view in self.viewSet) {
dispatch_async(dispatch_get_main_queue(), ^{
[view removeFromSuperview];
});
}
or
dispatch_async(dispatch_get_main_queue(), ^{
for (UIView *view in self.viewSet) {
[view removeFromSuperview];
}
});
To answer the question specifically, it breaks down into the size of units of work. If you dispatch a really large chunk of work to the main thread, then that large chunk will block the main thread for the entire time it is processing. If you dispatch a bunch really small chunks of work to the main thread, then the main thread will interleave processing of the chunks of work with processing of other events which will make your app slightly less efficient, but overall more responsive.
However, in the context of the question asked, neither dispatch_async() pattern is likely to be what you really want. You really shouldn't be removing a ton of views from any superview, whether it be all at once or interleaved on the main queue.
Instead, you should have a container view that contains the views that need to be swapped out-- potentially managed by a view controller-- and you should have one removeFromSuperview that removes that container. Once removed, then there really shouldn't be a need to manually remove any of the container views as you should have defined the retain/weak ownership rules such that you don't have cycles through view -> subview relationships.
Or, succinctly:
dispatch_async(.. main queue .., ^{
[_myContainerView removeFromSuperview];
});
In an ideal world, that removeFromSuperview would remove the last reference to _myContainerView and then it'd automatically tear down the hierarchy of subviews as the UIKit sees fit.
The first one
for (UIView *view in self.viewSet) {
dispatch_async(dispatch_get_main_queue(), ^{
[view removeFromSuperview];
});
}
queues up a bunch of [view removeFromSuperview] calls on the main thread. After the loop is over, they are executed one at a time. Each call is stuck waiting until the main queue clears, like a train waiting to pull into a station.
The second one
dispatch_async(dispatch_get_main_queue(), ^{
for (UIView *view in self.viewSet) {
[view removeFromSuperview];
}
});
queues just one thing onto the main thread, namely the whole loop. As soon as the main thread is free, a single piece of code runs, in which all the views are removed, kaboom.
I need help incorporating the
PreformSelector: WithObject: AfterDelay:
With the SKScene being paused.
I am making a game where there is a pause button which pauses the scene. This, however does not affect the
PreformSelector: WithObject: AfterDelay:
function which calls the "Reload" function. Without this fixed the user can fire a shot, press pause, wait for the reload function to be called, un-pause, and shoot. This makes it so the user can continuously fire without having to reload in "game time". Is there any way to fix this?
You shouldn't be using performSelector:withObject:afterDelay:, because keeping track of timing and storing the selectors and objects etc. with that method would be very cumbersome.
Instead, make use of SpriteKit's +[SKAction waitForDuration:] action.
You would want to do something like the following (I'm assuming this code takes place somewhere in one of your scene's methods):
// Replace 2.0 below with however long you want to wait, in seconds
SKAction *waitAction = [SKAction waitForDuration:2.0];
// I'm assuming your "Reload" method is a method declared in your scene's
// class and not some other class, so I'm using "self" as the target here.
SKAction *reloadAction = [SKAction performSelector:#selector(Reload) onTarget:self];
SKAction *sequenceAction = [SKAction sequence:#[waitAction, reloadAction]];
// Since I'm assuming this is in your scene implementation, `self` here
// refers to your scene node.
[self runAction:sequenceAction];
Since actions can be paused and resumed, when you pause your scene with self.paused = YES; your actions will also get paused and will resume where they left off when you later unpause your scene.
i have this code to wait for a loading task, showing a activityIndicator view
if (isLoading) {
self.tipView = [[BBTipsView alloc] initWithMessage:#"loading..." showLoading:YES parentView:self.view autoClose:NO];
self.tipView.needsMask = YES;
[self.tipView show];
while (isLoading) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
[self.tipView close];
}
the loading view will animate until the isLoading become false.here is my problem:
running a runloop in main thread will block the main thread until there is a source event comes or timer fire. but why the loading view keep animating while the main runloop didn't return?
-----edit by bupo----
I found that when timer fire the runloop won't return. This will make sense that the animation refresh ui by CADisplayLink timer fire.
Note that from the perspective of NSRunloop, NSTimer objects are not "input"—they are a special type, and one of the things that means is that they do not cause the run loop to return when they fire.
The NSRunLoop method runMode:beforeDate: runs until the given date OR until it finds a single event to deal with - after which the call returns. You're calling it on the main run loop ([NSRunLook currentRunLoop]). Hence, even though you think you're blocking the main run loop, you're not - you're causing events to be serviced. Hence, the animation timer can function, even though you might think you're 'blocking' the main run loop.
To confirm this, comment out the call to runMode:beforeDate: and you should see that the UI freezes until the operation completes.
Edit: see CodaFi's comment on your question. What actually happens if you comment out the call to runMode:beforeDate:, out of interest?
Original answer:
This style of code isn't recommended for starting and stopping UI animations. Don't mess around with run loops unless you have to. And having a tight loop that checks for boolean flag changing from elsewhere is often a code smell that means there's a better way.
Instead, do it asynchronously and without sitting on the main thread:
// on main thread
self.tipView = [[BBTipsView alloc] initWithMessage:#"loading..." showLoading:YES parentView:self.view autoClose:NO];
self.tipView.needsMask = YES;
[self.tipView show];
} // end of the method
- (void)loadingHasFinished {
// assuming this method called on main thread
[self.tipView close];
}
Obviously you'll have to ensure the loadingHasFinished is called as appropriate.
If loadingHasFinished is called on a background thread rather than the main thread, you'll want something like this:
- (void)loadingHasFinished {
dispatch_async(dispatch_get_main_queue(), ^{
[self.tipView close];
});
};
I would like show my view immediately when I call it. I don't know how to make the view show.
-(IBAction) showProgress: (id) sender {
progressViewController *progress = [[progressViewController alloc] initWithNibName:#"progressViewController" bundle:NULL];
[self.view addSubview:progress.view];
[self someFunctionWhichTakesAgesToBeDone];
}
It's called from current UIViewController. And the view appears after the long function. How can I show it before the long funcion? Thanks for answer.
Use GCD (Grand Central Dispatch) which is the simplest way (and recommended by Apple), the code will then be:
-(IBAction) showProgress: (id) sender {
progressViewController *progress = [[progressViewController alloc] initWithNibName:#"progressViewController" bundle:NULL];
[self.view addSubview:progress.view];
// Heavy work dispatched to a separate thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(#"dispatched");
// Do heavy or time consuming work
[self someFunctionWhichTakesAgesToBeDone];
// When finished call back on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
// Return data and update on the main thread
});
});
}
It´s two blocks. The first one does the heavy work on a separate thread and then a second block is called when the heavy work is finished so that changes and UI updates are done on the main thread, if needed.
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/nsobject_Class/Reference/Reference.html
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait`
use
[self.view performSelectorOnMainThread:#selector(addSubview:) withObject:progress.view waitUntilDone:YES]
or put your Sleep() function (i hope it's anything else, Sleep() func is really bad, as its been told) into another function MySleepFunc and call
[self performSelector:#selector(MySleepFunc) withObject:nil afterDelay:0.003]
instead of Sleep(3).
Read about multi-threading. In short, there's single UI thread that does drawing, accepting user events and so on. If you pause it with sleep() or any other blocking method, nothing will be shown/redrawn and no events will be processed. You have to make your HTTP request from background thread.
This is by far the weirdest problem I've been stuck with.
I have a UIViewController on a UINavigationController and I want to call a method at viewDidAppear using NSInvocationOperation so it can run on a back thread when the view becomes visible.
The problem is that if I pop the view controller BEFORE the operation (in this case the testMethod method) completes running, the app crashes.
Everything works fine if I pop the view controller AFTER the operation runs it's course.
When the app crashes, it stops at [super dealloc] with "EXC-BAD-ACCESS" and gives me the following error.
bool _WebTryThreadLock(bool), xxxxxxxxx: 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...
And this is my code (super simplified)..
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSInvocationOperation *theOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(testMethod) object:nil];
[operationQueue addOperation:theOperation];
[theOperation release];
}
- (void)testMethod
{
NSLog(#"do some stuff that takes a few seconds to complete");
}
- (void)dealloc
{
[_tableView release];
[super dealloc];
}
The testMethod has some code that takes a few seconds to complete. I only have a few clues and I really don't know how and where to start debugging this.
Clue #1: The funniest thing is that if I remove the [_tableView release]; from dealloc then the app doesn't crash. But of course this would cause a leak and I can't remove it.
Clue #2: I've tested this code on a separate "clean" UIViewController with a UITableView and to my surprise it didn't crash.
Clue #3: The app doesn't crash is the UITableView's datasource is set to nil in viewDidLoad.
Clue #4: The app doesn't seem crash if I use the same code in viewDidAppear somewhere else like an IBAction.
Clue #5: I've tried looking over stack data with NSZombie but it gives me tons of data and it leads me nowhere.
I have some very complicated code within my UITableViewDelegate and UITableViewDataSource and I really don't know where to start debugging this. I really hope I don't have to go through line by line or rewrite the entire thing because of this.
Any pointers on where I should be looking?
The problem is likely that your view controller's last reference is the operation queue holding onto it, which means you are technically calling (or having the system call) some UIKit methods in a background thread (a big no-no) when the operation cleans up.
To prevent this from happening, you need to send a keep-alive message to your controller on the main thread at the end of your operation, by adding something like this to the last line in your testMethod:
[self performSelectorOnMainThread:#selector(description) withObject:nil waitUntilDone:NO];
There is still a chance that this message may get processed before the operation queue releases your view controller, but that chance is pretty remote. If it's still happening, you could do something like this:
[self performSelectorOnMainThread:#selector(keepAlive:)
withObject:[NSNumber numberWithBool:YES]
waitUntilDone:NO];
- (void)keepAlive:(NSNumber *)fromBackground
{
if (fromBackground)
[self performSelector:#selector(keepAlive:) withObject:nil afterDelay:1];
}
By sending a message to your view controller on the main thread, it will keep the object alive (NSObject retains your view controller until the main thread handles the message). It will also keep the view controller alive if you perform a selector after a delay.
You're crashing because the controller is still trying to use your tableView reference and since you poped the viewController, everything will go away in the dealloc and the tableView is still populating itself.
You can try asking in your dealloc method if your operation is still running, so you can cancel it and the everything should be fine.
Once you add an operation to a queue, the operation is out of your
hands. The queue takes over and handles the scheduling of that task.
However, if you decide later that you do not want to execute the
operation after all—because the user pressed a cancel button in a
progress panel or quit the application, for example—you can cancel the
operation to prevent it from consuming CPU time needlessly. You do
this by calling the cancel method of the operation object itself or by
calling the cancelAllOperations method of the NSOperationQueue class.
Cancelling an operation does not immediately force it to stop what it
is doing. Although respecting the value returned by the isCancelled is
expected of all operations, your code must explicitly check the value
returned by this method and abort as needed. The default
implementation of NSOperation does include checks for cancellation.
For example, if you cancel an operation before its start method is
called, the start method exits without starting the task.