I want my Mac cocoa application to continuously update the View even if it goes to back ground? Something like Activity Monitor or DigitalColor Meter.
How can I achieve such behavior?
Thank you!
EDIT: I get context from NSGraphicContext and then create a thread to do the job in back ground.
- (void)drawRect:(NSRect)dirtyRect
{
context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
[NSThread detachNewThreadSelector: #selector(ExecuteDrawingInBackGround:) toTarget: self withObject: nil];
}
You can't use the Mac UI API from a background thread because it's not thread safe.
Programs that redraw themselves continuously do so by having the background threads (if any) notify the main thread that something has changed and needs redrawing. This is easiest done by having the background thread send -performSelector:OnMainThreadWithObject:waitUntilDone: to some object, usually the document or a view controller.
Alternatively, you can also use NSTimer to fire regularly and avoid background threads altogether.
Related
The problem: Attempting to display a window with text from applicationWillFinishLaunching will NOT draw itself if other processor-intensive non-UI code is immediately called.
Background: I have a helper app that when launched may or may not interact with the end user. While it is "deciding" if it needs to put up a window to ask user questions, there may be anywhere from 1 second to 10 seconds that elapse (after launch it's off in non-UI capable library code communicating over the internet).
So I wanted to be kind to the user and put up a "mini-alert"* window with "working, please wait...", prior to heading into that library code, which I will dismiss once that processing has elapsed.
It seems as if the app itself doesn't have time after launch to even draw this mini-alert (it's just an NSWindow, with an NSView, some text, and no buttons).
If after the library code returns and want to put up either an error alert or a query window for the user -- then at that point the mini-alert draws as expected. However, if I close the mini-alert (see below) and then put up an NSAlert -- the mini-alert doesn't have enough time to dismiss itself.
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
[NSApp activateIgnoringOtherApps:YES];
briefAlertWindowController = [[NSWindowController alloc] initWithWindowNibName:#"BriefAlertWindow"];
[[briefAlertWindowController window] center];
[briefAlertWindowController showWindow:self ];
[[briefAlertWindowController window] orderFront:self ];
[[briefAlertWindowController window] display];
[[briefAlertWindowController window] makeKeyAndOrderFront:nil];
}
and dismissing the mini-alert:
- (void)dismissMiniAlert
{
NSWindow * theWindow = [briefAlertWindowController window];
[theWindow orderOut:nil];
}
NOTE that neither NSWindow not NSWindowController have been derived/subclassed for this mini-alert.
I'm using the term "mini-alert", because I've noticed people get annoyed about the concept of a "splash screen". While the functionality IS similar -- I'm really just trying to let the user know that an unavoidably long operation is taking place.
It sounds like a threading problem. The splash window can't draw itself on the main thread because the main thread is busy doing the processor-intensive operation. Properly, your processor-intensive stuff should all be happening on a background thread. If you can't do that, you need at least to get off the main thread long enough to give the runloop a chance to draw your window. Just introduce a delay.
I am animating a series of PNG sequences of about 250 frames. I am doing this instead of running it as a H264 because I need background transparency.
For some reason even though I load my NSArray and load the startAnimating call inside of an NSInvocationOperation, the UI appears to freeze after startAnimating is called. I can see that while the images are being loaded into the array the app is still responsive. I am not sure how to stop startAnimating from blocking the main thread.
Thanks!
It is important that your call to startAnimating be done on the main thread - all UIKit activity must occur on the main threat. You can do that by changing your method call to something like this:
[imageView performSelectorOnMainThread:#selector(startAnimating) withObject:nil waitUntilDone:NO];
ended up switching to CoreMedia
Newbie Warning
I have a simple but vexing problem trying to disable an NSButton. Here is sample code to illustrate the problem:
- (IBAction)taskTriggeredByNSButtonPress:(id)sender {
[ibOutletToNSButton setEnabled:NO];
//A task is performed here that takes some time, during which time
//the button should not respond to presses.
//Once the task is completed, the button should become responsive again.
[ibOutletToNSButton setEnabled:YES];
}
This is what I observe. I press the button. The button becomes disabled (judging by its faded appearance), and the task begins executing. While the button is disabled and the task is executing, I press the button a second time. Nothing happens immediately, but once the task is completed, the taskTriggeredByNSButtonPress: method is called a second time, suggesting that the second button press was placed on hold and then activated once the button became re-enabled.
I've tried all kinds of hacks to prevent the second button press from being recognized, including introducing a time delay after the [ibOutletToNSButton setEnabled:NO]; statement, making the button hidden rather than disabled, covering the button with a custom view during the time it should be disabled, binding the button's enabled status to a property, and other things I'm too embarrassed to mention.
Please help me understand why I can't get this simple task of disabling the button to work.
This method seems to be directly linked to the button. You should perform the long action on another thread, or the main runloop won't be available until the method returns. The main runloop doesn't respond to events while it's not available.
First, create a method:
- (void)someLongTask: (id)sender {
// Do some long tasks…
// Now re-enable the button on the main thread (as required by Cocoa)
[sender performSelectorOnMainThread: #selector(setEnabled:) withObject: YES waitUntilDone: NO];
}
Then, perform that method in a separate thread when the button's clicked:
- (IBAction)buttonPushed: (id)sender {
[sender setEnabled: NO];
[self performSelectorInBackground: #selector(someLongTask) withObject: nil];
}
You may replace self in the example above with the object where -someLongTask resides.
By multi-threading, you leave the main runloop alone and stable. Maybe, your problem will be solved. Otherwise, you solved a problem with responsiveness.
(By the way, if the method is only called by the button, the sender argument is set to the button. That way, you don't need to use an outlet in the method. But that's just a hint.)
You should not perform tasks that require a lot of processing time on the main event loop. This is what you are doing, and the app's entire UI will block while your code executes. Blocking the main thread is the cause of the Spinning Pizza of Death™. In other words, don't do it.
What you need to do instead is break out your time-consuming code so that it runs in another thread, that is, concurrently in the background. When the background task completes, it should somehow notify the code running in the main thread that it has finished. Your code in the main thread can then update the UI appropriately.
There are many ways to do this.
You can use the NSThread methods as suggested by Randy Marsh. However, you must be very careful to read the documentation, as you can't just call any old method on a background thread and expect it to work. You must create your own autorelease pool in the thread and dispose of it correctly. You must NOT call any methods that update the UI from a secondary thread. You must be extremely careful that no variables will be accessed or modified by more than one thread at a time. Threading is a complex business.
The -performSelectorInBackground:withObject: method of NSObject is essentially a simple way of using NSThread and has the same provisos.
You can use the NSOperation and NSOperationQueue methods, which are especially good if you can break down your task's work into small chunks that can be executed simultaneously.
The simplest way of handling this is GCD (Grand Central Dispatch), which allows you to use inline blocks to write background processes:
- (IBAction)taskTriggeredByNSButtonPress:(id)sender
{
[ibOutletToNSButton setEnabled:NO];
//get a reference to the global GCD thread queue
dispatch_queue_t queue = dispatch_get_global_queue(0,0);
//get a reference to the main thread queue
dispatch_queue_t main = dispatch_get_main_queue();
//perform long-running operation
dispatch_async(queue,^{
NSLog(#"Doing something");
sleep(15);
//update the UI on the main thread
dispatch_async(main,^{
[ibOutletToNSButton setEnabled:YES];
});
});
}
GCD is very lightweight and efficient and I highly recommend you use it if possible.
There is a lot more information and detail in Apple's Concurrency Programming Guide, which I recommend you read, even though some of the detail may be beyond you at this stage.
I started a thread in a UIview as a background thread which transfer data for the view. However, crash happens in such situation: When I left the view at the very time that the thread is trying to transfer data.
I didn't get quite clear with the relationship between the UIview object and thread. I guess it crashes because the thread was trying to visit UIview members or methods, which were not existed any more. So, I wonder what happened to the thread if the UIView which detach it has been left.
This is my detaching code:
- (void)reloadData {
isLoaded = NO; //UIView member.
[NSThread detachNewThreadSelector:#selector(getThreadInAnotherThread) toTarget:self withObject:nil];
}
- (void)getThreadInAnotherThread {
//Loading code
isLoaded = YES;
[self performSelectorOnMainThread:#selector(reloadTable) withObject:nil waitUntilDone:YES];
}
And I didn't do anything in viewDidDisappear.
As the documentation for detachNewThreadSelector:toTarget:withObject states:
The objects aTarget and anArgument are retained during the execution of the detached thread, then released.
And the same for performSelectorOnMainThread:withObject:waitUntilDone:
This method retains the receiver and the arg parameter until after the selector is performed.
If its still not clear what is happening here consult the cocoa memory management guide.
There is no relationship between the view and the thread unless you put one there yourself.
I have an NSWindow subclass (GameWindow) containing an NSOpenGLView subclass (GameView).
The app is windowed (does not go fullscreen).
An OpenGL animation in GameView is fired ~30 times a second by a timer.
For presentation reasons, the GameView animation MUST continue regardless of what else is happening in the app. The only time it should stop is in the case of a fatal error.
I need to present various "modal" Cocoa windows (e.g. choose new game, confirm quit, etc.) while the animation in GameWindow continues. Some of these could be sheets, but major ones need to appear as standalone windows (complete with WebViews).
MY QUESTION: how can I display these "dialog" windows such that my app timer continues to fire, my animation continues, but user input to the GameView in the GameWindow is blocked until the "dialog" window is dismissed by the user?
(I need to support Tiger + Leopard at this time).
Have you tried the regular sheet/dialog techniques? They should work fine for this situation. Timers are scheduled as part of the run loop, which doesn't stop when you have a modal sheet or window, so it should be able to continue on rendering in the background while events are blocked.
[NSApp beginSheet:sheetWindow modalForWindow:mainWindow modalDelegate:nil didEndSelector:NULL contextInfo:nil];
(Except fill in your own delegate and end selector if needed.)
If you want to keep the current modal windows (without moving to sheets), you can try scheduling the NSTimer yourself in something besides the default runloop mode (NSDefaultRunLoopMode), which hangs as soon as that runloop stops running.