How do I prevent an NSButton from registering presses while disabled? - cocoa

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.

Related

performSelector afterdelay still running even though I have unloaded the viewcontroller that it originated from

In Xcode I have a view which does a modal segue to tab bar controller, and the first view in the tab bar is a UIViewController. This view performs a task on a timed basis (every 15 seconds) using [self performSelector:#selector(blah:) withObject:nil afterDelay:15]. I have some delegate methods that take me back to the root view and calls [self dismissViewControllerAnimated] which in my mind would unload the tab bar controller which would in turn unload the view that is performing the selector every 15 seconds. While those views do disappear after I call dismissViewControllerAnimated, the selectors continue to run every 15 seconds (I see them logging messages in the Xcode console). Am I wrong in my logic that these selectors should cease since their viewcontroller has been unloaded?
Not always. It depends on your classes, if you have a strong reference to the controller or the timer somewhere it may not actually be released. It could also be that since the timer was never invalidated it's keeping the view controller from releasing. Timers are interesting in that they can cause behavior like this if not invalidated, it's always good practice to clean them up when you're done with them. If you're using recursion to call the timed method on itself every 15 seconds that could also keep the view from being released.
If you're using a timer you'll want to invalidate it when appropriate, on view disappear probably. If you want to run once after view disappear you'll need to set a bool so you know when the view disappeared so you can run it once more.
If you're using recursion, be really careful. Something like that can run on a background thread indefinitely. Again if you only want it to run while the view is the active view you can do a check to see which view is on top and only call the method recursively in that scenario, or set a bool on appear and disappear and check the bool when the method calls itself.

Proper way of showing a NSWindow as sheet

Function NSBeginAlertSheet(...) has all the events I need to have especially the didDismiss: callback, but I really need to be able to do the same sheet action with any window I want, so I discovered this notification:
NSWindowDidOrderOffScreenAndFinishAnimatingNotification
Which is posted whenever a sheet is closed AND done with animations
now, my question is can I use that? Or is there a better way?
I use ARC and I load the windows from .xib using NSWindowController.
Overall what I need is to show a window as sheet and catch all events.
What's wrong with
- (void)beginSheet:(NSWindow *)sheet modalForWindow:(NSWindow *)docWindow modalDelegate:(id)modalDelegate didEndSelector:(SEL)didEndSelector contextInfo:(void *)contextInfo
This calls the optional didEndSelector which should look like this:
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
This is all in the NSApplication documentation. There are two methods for ending the sheet:
- (void)endSheet:(NSWindow *)sheet returnCode:(NSInteger)returnCode
- (void)endSheet:(NSWindow *)sheet
So you could just do whatever you wanted to right before calling endSheet: or you could in the sheetDidEnd: method.
Edit:
Here is an example project showing that after calling [window orderOut:self] then animation is finished and you can do what you'd like.
NSWindowDidEndSheetNotification It is posted whenever a sheet is finished animating out.
Starting with 10.9, the correct way is to call beginSheet:completionHandler: on a NSWindow object.
This method has the advantage, that the completion handler is a block, so it can keep all the objects alive that are required as long as the sheet is still displayed and once the sheet is done and the block has been executed, the block itself is released and thus all objects it was keeping alive are as well.
To make sure a block keeps objects alive, use the objects within that block or if there is no way to use them in a meaningful fashion, put all of them into a NSMutableArray and within the block call removeAllObjects on that array; this requires the block to keep the array alive and the array keeps the rest alive -> memory management made easy.

NSArrayController rearrangeObjects error

I have troubles with NSArrayController rearrangeObjects function - this function called from some background treads and sometimes App crashes with error: 'endUpdates called without a beginUpdates'.
How can i detect if arrayController is rearranging objects in current moment and add next rearrange to some like queue or cancel current rearranging and run new?
May be there is another solution for this problem?
Edit code added:
TableController implementation:
- (void)setContent{//perfoms on main thread
//making array of content and other functions for setting-up content of table
//...
//arrayController contains objects of MyNode class
//...
//end of setting up. Call rearrangeObjects
[arrayController rearrangeObjects];
}
- (void)updateItem:(MyNode *)sender WithValue:(id)someValue ForKey:(NSString *)key{
[sender setValue:someValue forKey:key];
[arrayController rearrangeObjects];//exception thrown here
}
MyNode implementation:
- (void)notifySelector:(NSNotification *)notify{
//Getted after some processing finished
id someValue = [notify.userInfo objectForKey:#"observedKey"];
[delegate updateItem:self WithValue:someValue ForKey:#"observedKey"];
}
Don't do that. AppKit (to which NSArrayController belongs) is not generally thread safe. Instead, use -performSelectorOnMainThread:... to update your UI (including NSArrayController). ALWAYS do updating on the main thread.
Joshua and Dan's solution is correct. It is highly likely that you are performing operations on your model object in a background thread, which then touches the array controller, and hence touches the table.
NSTableView itself is not threadsafe. Simply adding in a "beginUpdates/endUpdates" pair will just avoid the race condition for a bit. However, like Fin noted, it might be good to do the pair of updates for performance reasons, but it won't help with the crash.
To find the sources of the crash, add some assertions in your code on ![NSThread currentThread].mainThread -- particularly any places before you touch the array controller's content. This will help you isolate the problem. Or, subclass NSTableView and add the assertion in somewhere key, like overriding -numberOfRows (which is called frequently on changes), and calling super.
-corbin
AppKit/NSTableView
I solved this by adding this at the very start of my UI initialisation
[myTableView beginUpdates];
and then at the end after the persistant store has been loaded fully:
[myTableView endUpdates];
This also make the app startup a lot better since it does not constantly have to reload all loads from the MOC.

How to force TouchesEnded

The user drags the game piece (the i-th image view) to its target location. The program counts, sees that all the items are in the correct places, announces "Game Over" and sets userInteractionEnabled to NO.
Great, except that if the user's finger is still down on the game piece, the user can drag the piece back out of the target area by accident. "Game Over" is showing but the piece is no longer in the correct place.
Is there a way to force a touchesEnded (not detect a touchesEnded) so that the contact with the game piece is (effectively) broken when the piece is in its final destination (i.e. so that the user can't accidentally pull it out of position)?
userInteractionEnabled = NO does not seem to take effect until the touch is released.
I'm not aware of a way to force touchesEnded.
Obviously one way to handle this is for your application to maintain state that indicates that the game is over and guard against moving any game pieces when in that state.
You might try beginIgnoringInteractionEvents, though I doubt this will do what you are looking for. I really think managing state in your application that ensures that you will do the right thing, not moving the piece once the end state has been reached, is the way to go.
From Apple's Event Handling Guide for iOS:
Turning off delivery of touch events for a period. An application can
call the UIApplication method beginIgnoringInteractionEvents and later
call the endIgnoringInteractionEvents method. The first method stops
the application from receiving touch events entirely; the second
method is called to resume the receipt of such events. You sometimes
want to turn off event delivery while your code is performing
animations.
I found this question after wanting to do a very similar action within my own application. I wanted the touchesEnded: method to be run immediately after clicking on the moving action (like a touchDown action method), but did not know how to achieve this. In the end this is what I did and it WORKED! :
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
...
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self touchesEnded:touches withEvent:event];
});
}
This worked perfectly!

Continuos UI update Mac Application?

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.

Resources