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];
});
};
Related
I have a really light ViewController, it does nothing in viewDidLoad. I'm pushing this view on top of a navigationController. The method who does this action is called from inside a block. After the call to showView I added an NSLog, and that log prints in the console really fast, but the view takes a lot to load... I really don't understand what maybe happening... any idea???
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
[self showView];
NSLog(#"EXECUTED");
});
- (void) showView{
TestViewController *test = [[TestViewController alloc]init];
[self.navigationController pushViewController:test animated:NO];
}
From the docs for ABAddressBookRequestAccessWithCompletion:
The completion handler is called on an arbitrary queue. If your app uses an address book throughout the app, you are responsible for ensuring that all usage of that address book is dispatched to a single queue to ensure correct thread-safe operation.
You should make sure your UI code is called on the main thread.
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error {
dispatch_async(dispatch_get_main_queue(), ^{
[self showView];
NSLog(#"EXECUTED");
});
});
This might not be the only problem, but according to the docs, the completion handler passed to ABAddressBookRequestAccessWithCompletion is called on an arbitrary queue. -showView should only be called on the main queue since it is dealing with UIViewController objects.
The other thing to ask is what else is happening on the main queue? Are there other long-running tasks that could be blocking UI updates?
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.
I have the following problem:
I want that the user make a download when pressing a button. During this download, I want to hide the other buttons (which would open the downloaded files, so I want to ensure that no one tries to open files when the update haven't finished yet).
Is it possible to hide these buttons during this process?
So what I have tried and experienced so far:
Changes to the buttons I get always just at the end (when it isn't necessary anymore, because then the update is done).
I tried the following (Pseudocode):
-(void)updatingprogress
{
buttona.hidden=TRUE;
}
-(void)updatingfinished
{
buttona.hidden=FALSE;
}
updateFiles()
{
[self updatingprogress]
... make downloads...
[self updatingfinished]
}
So with logging I see, that I get in my functions at the moment I want, but the changes of the buttons aren't done during "updatingprogress". Any Idea how to solve this problem?
Thanks and best regards!
A common problem is that you are trying to update UI elements on a background thread. If your updateFiles method is happening on a different thread your button may not be hidden properly. To dispatch methods to the main threads you can either use the NSOperationQueue API or the GCD API.
NSOperationQueue:
[[NSOperationQueue mainQueue] addBlockOperation:^ {
buttona.hidden = YES;
}];
GCD:
dispatch_async(dispatch_get_main_queue(), ^ {
buttona.hidden = YES;
});
Both of these APIs do the same thing. I generally try to use the highest abstraction possible so in this case I would use the NSOperationQueue method
Another possibility is that you're doing all the work on the main thread, but failing to allow for the fact that, as a rule, UIKit changes don't take effect until you drop down to the runloop.
The background logic is that you don't want partial changes to be visible, so e.g. if you wrote:
// okay, set to success
label.textColor = [UIColor greenColor]; // was previously red
label.text = #"Success"; // previously said 'Failure'
What you explicitly don't want is for the word 'Failure' to appear in green, then for the word to change to 'Success'. You want the two changes to occur atomically. Apple achieve this by batching UIKit updates together and effecting them outside of any of your scheduled methods.
So if you have a function on the main thread that does some UI changes, does some work and then undoes the UI changes, but all without at any point exiting to the runloop, then the changes will never be seen.
The quickest solution would be:
- (void)updateFiles
{
[self updatingProgress];
[self performSelector:#selector(doFileUpdate) withObject:nil afterDelay:0.0];
// what does the above achieve? It schedules doFileUpdate on the runloop, to
// occur as soon as possible, but doesn't branch into it now. So when this
// method returns UIKit will update your display
}
- (void)doFileUpdate
{
/* heavy lifting here */
[self updatingFinished];
}
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.
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
});