I've read all related questions I could find but I'm still stuck, so I hope someone will spot my reasoning error.
I'm trying to periodically update some UIView. For simplicity, I've reduced the code to what's below. Summary: In viewDidLoad, I call a method on a new background thread. That method calls a method on the main thread which is supposed to update some UILabel. The code seems to work correctly: the background thread is not the main thread and the method calling the UILabel update is on the main thread. In code:
In viewDidLoad:
[self performSelectorInBackground:#selector(updateMeters) withObject:self];
This creates a new background thread. My method updateMeters (for simplicity) now looks like this:
if ([NSThread isMainThread]) { //this evaluates to FALSE, as it's supposed to
NSLog(#"Running on main, that's wrong!");
}
while (i < 10) {
[self performSelectorOnMainThread:#selector(updateUI) withObject:nil waitUntilDone:NO];
//The code below yields the same result
// dispatch_async(dispatch_get_main_queue(), ^{
// [self updateUI];
// });
[NSThread sleepForTimeInterval: 1.05];
++i;
}
Finally, updateUI does just that:
if ([NSThread isMainThread]) { //Evaluates to TRUE; it's indeed on the main thread!
NSLog(#"main thread!");
} else {
NSLog(#"not main thread!");
}
NSLog(#"%f", someTimeDependentValue); //logs the value I want to update to the screen
label.text = [NSString stringWithFormat:#"%f", someTimeDependentValue]; //does not update
For all I know, this should work. But it doesn't, unfortunately... The commented out dispatch_async() yields the same result.
Most likely you have your format statement wrong.
label.text = [NSString stringWithFormat:#"%f", someTimeDependentValue];
Make sure that someTimeDependentValue is a float. If it is an int it will likely get formatted to 0.0000.
Here's a repo showing a working version of what you describe. Whatever is wrong is not related to the threading.
To expand on my comment, here's a scenario that might be best achieved using a NSTimer:
-(void)viewDidLoad
{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:<number of seconds per tick> target:self selector:#selector(timerTick:) userInfo:nil repeats:YES];
}
-(void)timerTick:(id)sender
{
label.text = ...;
}
There is a more elaborate approach which I use extensively in my projects. And that is the concept of an engine.
I would have an engine that runs in the background using a timer. And at key moments, it would post a notification using NSNotificationCenter on the main thread using dispatch_async/dispatch_get_main_thread() and any one of your views can then subscribe and handle that notification by updating their UI.
Related
I wan't to show Progress indicator (MBprogress hud), here's the code i implemented.
[NSThread detachNewThreadSelector: #selector(showMe) toTarget:self withObject:NULL];
in show method , i have (tried to) displayed MBprogress Hud but it is not showing label text.
-(void)showMe
{
if(hudForBal) // hudForBal is my MBprogressHud's object
{
[hudForBal removeFromSuperview];
[hudForBal release];
hudForBal = nil;
}
hudForBal =[[MBProgressHUD alloc]init];
hudForBal.labelText =#"Please wait...";
hudForBal.delegate = Nil;
[self.view addSubview:hudForBal];
[hudForBal show:YES];
}
it is working but it is not showing label text .What am i doing wrong?
Thanks in advance!
There's no need to create a new thread to do this, in fact, making changes to the UI on any thread other than the main thread is undefined behavior. If you're already on the main thread when you would be calling this method, then all you have to do is perform the selector as you normally would, without sending it to a different thread.
However, if you're already on a background thread when you would be performing this selector, and you want to update the UI, you can use dispatch_async() as a quick and easy way to move back to the main thread.
- (void)showMe {
dispatch_async(dispatch_get_main_queue(), ^{
if(hudForBal) {
[hudForBal removeFromSuperview];
[hudForBal release];
hudForBal = nil;
}
hudForBal =[[MBProgressHUD alloc]init];
hudForBal.labelText =#"Please wait...";
hudForBal.delegate = Nil;
[self.view addSubview:hudForBal];
[hudForBal show:YES];
});
}
In Grand Central Dispatch I want to start a spinner - UIActivityIndicatorView - spinning prior to beginning long running task:
dispatch_async(cloudQueue, ^{
dispatch_async(dispatch_get_main_queue(),
^{
[self spinnerSpin:YES];
});
[self performLongRunningTask];
dispatch_async(dispatch_get_main_queue(),
^{
[self spinnerSpin:NO];
});
});
Here is the spinnerSpin method:
- (void)spinnerSpin:(BOOL)spin {
ALog(#"spinner %#", (YES == spin) ? #"spin" : #"stop");
if (spin == [self.spinner isAnimating]) return;
if (YES == spin) {
self.hidden = NO;
[self.spinner startAnimating];
} else {
[self.spinner stopAnimating];
self.hidden = YES;
}
}
One thing I have never seen discussed is the difference - if any - between [myView setNeedsDisplay] and [myActivityIndicatorView startAnimating]. Do they behave the same?
Thanks,
Doug
The [UIView setNeedsDisplay] method has nothing to do with a UIActivityIndicatorView's animation state.
setNeedsDisplay simply informs the system that this view's state has changed in a way that invalidates its currently drawn representation. In other words, it asks the system to invoke that view's drawRect method on the next drawing cycle.
You very rarely need to invoke setNeedsDisplay from outside of a view, from code that is consuming the view. This method is meant to be invoked by the view's internal logic code, whenever something changes in its internal state that requires a redraw of the view.
The [UIActivityIndicatorView startAnimating] method is specific to the UIActivityIndicatorView class and simply asks the indicator to start animating (e.g. spinning). This method is instant, without requiring you to call any other method.
On a side note, you could simplify your code by simply calling startAnimating or stopAnimating without manually showing/hiding it. The UIActivityIndicatorView class has a hidesWhenStopped boolean property that defaults to YES, which means that the spinner will show itself as soon as it starts animating, and hide itself when it stops animating.
So your spinnerSpin: method could be refactored like this (as long as you haven't set the hidesWhenStopped property to NO):
- (void)spinnerSpin:(BOOL)spin {
if (YES == spin) {
[self.spinner startAnimating];
} else {
[self.spinner stopAnimating];
}
}
I am having problems updating my customView object during simulation. The window pops up after the simulation is done. I would like it to update itself during the simulation. For this I use setNeedsDisplay:YES and I have also tried display. None of this works for me however. Does anyone have an idea how I should get this working? As you can see below I have tried to create a new thread for the updating as well as using NSOperations. Grateful for help!
//Run simulation
for (int iteration=0; iteration<numberOfIterations; iteration++){
//NSInvocationOperation *update = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(updatePopulation) object:nil];
//NSInvocationOperation *draw = [[NSInvocationOperation alloc] initWithTarget:view selector:#selector(redraw) object:nil];
//[draw addDependency:update];
//[queue addOperation:update];
//[queue addOperation:draw];
[NSThread sleepForTimeInterval:0.01]; //to make it easer to see..
[self updatePopulation];
//[view redraw];
[NSThread detachNewThreadSelector:#selector(redraw) toTarget:view withObject:nil];
//[self performSelector:#selector(updatePopulation) withObject:nil afterDelay:1];
//[view performSelector:#selector(redraw) withObject:nil afterDelay:1];
//Save segregation
if (iteration%(numberOfIterations/100) == 0) {
printf("hej\n");
}
}
in my viewer class:
- (void) redraw {
//[self setNeedsDisplay:YES];
[self display];
}
It looks like you are trying to do the painting on the worker thread.
This is not, as far as I am aware, supported.
To solve this, you need to move your simulation to a worker thread, and then use performSelectorOnMainThread: to invoke the redraw on the main thread. I find this article on threading in cocoa to be required reading when trying to implemented threaded cocoa apps.
I have an audio file which needs to fade out while the user is scrolling a UIScrollView. However, any performSelector:withObject:afterDelay: method is blocked until the user has stopped scrolling. So I have tried to create some code to perform a fadeout on another thread:
- (void)fadeOut
{
[NSThread detachNewThreadSelector:#selector(fadeOutInBackground:) toTarget:self withObject:self.audioPlayer];
}
- (void)fadeOutInBackground:(AVAudioPlayer *)aPlayer
{
NSAutoreleasePool *myPool = [[NSAutoreleasePool alloc] init];
[self performSelector:#selector(fadeVolumeDown:) withObject:aPlayer afterDelay:0.1];
[myPool release];
}
- (void)fadeVolumeDown:(AVAudioPlayer *)aPlayer
{
aPlayer.volume = aPlayer.volume - 0.1;
if (aPlayer.volume < 0.1) {
[aPlayer stop];
} else {
[self performSelector:#selector(fadeVolumeDown:) withObject:aPlayer afterDelay:0.1];
}
}
It gets as far as the performSelector, but no further because I guess it's trying to perform on a thread it has no access to. I can't even change it for performSelector:onThread:withObject:waitUntilDone: because there is no delay option.
Any ideas? Why have they made it so hard to just fade out a sound? moan
Thanks!
I resolved a similar issue by scheduling the selector in a different run loop mode than the default one. This way it is not interfering with the scrolling events. Using the NSRunLoopCommonModes worked for me:
[self performSelector:#selector(fadeVolumeDown:)
withObject:aPlayer
afterDelay:0.1
inModes:[NSArray arrayWithObject: NSRunLoopCommonModes]];
Inspired by the answer above
while (theAudio.volume > 0.000000)
[self fadeVolumeDown];
- (void) fadeVolumeDown{
theAudio.volume -=0.01;
}
I have an IBAction with some simple code inside:
-(IBAction)change:(id)sender {
[textfield setHidden:NO];
[self dolengthyaction];
}
'textfield' is an NSTextField in a nib file, and -'dolengthyaction' is a function that takes about a minute to finish executing.
My question is: Why isn't the textfield shown until AFTER "dolengthyaction" is done executing? I want it to be revealed before the dolengthyaction starts taking place. Is this an inherent problem or is there something wrong with my code? (or in another part of my code?)
I'm still not very good at programming so I apologize if I worded something badly and formatted something wrong.
EDIT: There isn't much else other than this IBAction and -dolengthyaction...
-(void)doLengthyAction {
sleep(10);
}
-(IBAction)change:(id)sender {
[textfield setHidden:NO];
[self doLengthyAction];
[textfield setHidden:YES];
}
All I really want to do is display the label when the action is running and hide it when it's done.
Basically what this means is that it's not displaying at all right now.
In reality, in -doLengthyAction it's not sleep(10) but rather a NSFileManager operation that copies about 50 Mb of material. The code was rather long, but if you want me to post it I can. I tested it with sleep() but it doesn't work either.
All drawing operations (including hiding and showing views) are triggered from the run loop. The run loop cannot perform the next event until after your function returns.
If you have an operation that takes more than a second to run, you should perform it in a thread. Once it completes, use performSelectorOnMainThread to make UI changes on the main thread.
As mentioned in some of the previous answers, the application must return to the main run loop before it will redraw (it's an optimization to avoid redrawing when you make many changes).
You really should handle things on a background thread if they're going to run for a long time. If you don't the UI will beach ball while your operation runs.
If you're targeting 10.6+, you can use GCD and blocks as follows to run things easily in the background (and not have to define multiple methods to get things done):
-(void)doLengthyAction {
sleep(10);
}
-(IBAction)change:(id)sender {
[textfield setHidden:NO];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self doLengthyAction];
dispatch_async(dispatch_get_main_queue(), ^{
[textfield setHidden:YES];
});
});
}
Of course by using this, you'll need to make sure that what's going on in the lengthy action is thread safe, but you'll end up with a better user experience. For more info on this type of CGD code, you might want to read this.
I think there is something wrong with the rest of your code. This should not happen.
Post more?
Can't say what exactly is transpiring with out looking at rest of the code, but a hackish way would be to try:
-(IBAction)change:(id)sender {
[textfield setHidden:NO];
[self performSelector:#selector(doLenghtyAction) withObject:nil afterDelay:0.1]; --> or maybe even 0.0
}
-(void)doLengthyAction {
sleep(10);
[textfield setHidden:YES];
}
-(IBAction)change:(id)sender {
[textfield setHidden:NO];
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(doLengthyAction) userInfo:nil repeats:NO];
}
Try this:
-(IBAction)change:(id)sender {
[textfield setHidden:NO];
[[textfield window] display]; // force the window to update
[self doLengthyAction];
[textfield setHidden:YES];
}