I am just experimenting with disabling a button with various methods on XCode MacOS (not iOS) Cocoa Objective-C.
In this scenario I have a help button (m_btHelp) that is disabled when g_bEnableHelpButton = NO; but it is only being checked when the mouse moves.
-(void)mouseMoved:(NSEvent *)theEvent
{
if(g_bEnableHelpButton) {
[m_btHelp setEnabled:YES];
} else {
[m_btHelp setEnabled:NO];
}
I would rather have this continuously checked instead of only checked when the mouse moves. I have tried NSTimer with something like this, but it does not seem to work (m_btHelp does not get disabled when g_bEnableHelpButton = NO; like it does in the mouseMoved event:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(Timerloop) userInfo:nil repeats:YES];
}
- (void)Timerloop
{
if(g_bEnableHelpButton) {
[m_btHelp setEnabled:YES];
} else {
[m_btHelp setEnabled:NO];
}
}
g_bEnableHelpButton is a global variable, right? Don't use global variables. It's way better to create a class which holds your state (can be view model, ...). I'm going to skip a state class in all examples below and will use BOOL helpButtonEnabled property on the same view controller (this is not mandatory, it just makes all these examples a bit shorter). You can move this property elsewhere, it can be a state class, it can be basically any object.
Another thing is this NSTimer, NSTrackingArea, ... With all these things one is wasting CPU cycles, battery life, ... Cocoa & Objective-C offers various ways how to monitor a property value and react to it. You can override property setter, you can use KVO or bindings. All three methods are covered in examples below.
There are other ways for sure (like ReactiveCocoa), but I'd like to demonstrate three ways how to achieve it without dependencies.
Initial state
Imagine you have this view:
With the following implementation:
#import "ViewController.h"
#interface ViewController ()
// Help button from the Main.storyboard
// Imagine it's your m_btHelp
#property (nonatomic, strong) IBOutlet NSButton *helpButton;
// Property driving helpButton enabled/disabled state
// Imagine it's your g_bEnableHelpButton
#property (nonatomic, getter=isHelpButtonEnabled) BOOL helpButtonEnabled;
#end
#implementation ViewController
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
There's help button and there's toggle button which just toggles helpButtonEnabled value (YES -> NO, NO -> YES).
How to monitor it without timer, tracking area, ... to update the help button state?
Override setter
Encapsulating Data.
#implementation ViewController
// This is setter for the helpButtonEnabled property.
- (void)setHelpButtonEnabled:(BOOL)helpButtonEnabled {
// If the new value equals, do nothing
if (helpButtonEnabled == _helpButtonEnabled) {
return;
}
// Update instance variable
_helpButtonEnabled = helpButtonEnabled;
// Update button state
_helpButton.enabled = helpButtonEnabled;
}
- (void)viewDidLoad {
[super viewDidLoad];
// When the view loads update button state to the initial value
_helpButton.enabled = _helpButtonEnabled;
}
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
KVO
Introduction to Key-Value Observing Programming Guide.
static void * const ViewControllerHelpButtonEnabledContext = (void*)&ViewControllerHelpButtonEnabledContext;
#implementation ViewController
- (void)dealloc {
// Remove previously registered observer when the view controller goes away
[self removeObserver:self forKeyPath:#"helpButtonEnabled" context:ViewControllerHelpButtonEnabledContext];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Register observer for the helpButtonEnabled key path
// - it fires immeditately with the current value (NSKeyValueObservingOptionInitial)
// - it fires later every single time new value is assigned (NSKeyValueObservingOptionNew)
// - context is used to quickly distinguish why the observeValueForKeyPath:... was called
[self addObserver:self
forKeyPath:#"helpButtonEnabled"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:ViewControllerHelpButtonEnabledContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == ViewControllerHelpButtonEnabledContext) {
// It's our observer, let's update button state
_helpButton.enabled = _helpButtonEnabled;
} else {
// It's not our observer, just forward it to super implementation
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
Binding
Introduction to Cocoa Bindings Programming Topics.
#implementation ViewController
- (void)dealloc {
// Remove binding when the view controller goes away
[self.helpButton unbind:NSEnabledBinding];
}
- (void)viewDidLoad {
[super viewDidLoad];
// self.helpButton.enabled is binded to self.helpButtonEnabled
[self.helpButton bind:NSEnabledBinding
toObject:self
withKeyPath:#"helpButtonEnabled"
options:nil];
}
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
Related
Below is my typical WindowController module for presenting a modal dialog (could be settings, asking username/password, etc) loaded from a XIB. It seems a bit too complex for something like this. Any ideas how this can be done better/with less code?
Never mind that it's asking for a password, it could be anything. What frustrates me most is that I repeat the same pattern in each and every of my XIB-based modal window modules. Which of course means I could define a custom window controller class, but before doing that I need to make sure this is really the best way of doing things.
#import "MyPasswordWindowController.h"
static MyPasswordWindowController* windowController;
#interface MyPasswordWindowController ()
#property (weak) IBOutlet NSSecureTextField *passwordField;
#end
#implementation MyPasswordWindowController
{
NSInteger _dialogCode;
}
- (id)init
{
return [super initWithWindowNibName:#"MyPassword"];
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self.window center];
}
- (void)windowWillClose:(NSNotification*)notification
{
[NSApp stopModalWithCode:_dialogCode];
_dialogCode = 0;
}
- (IBAction)okButtonAction:(NSButton *)sender
{
_dialogCode = 1;
[self.window close];
}
- (IBAction)cancelButtonAction:(NSButton *)sender
{
[self.window close];
}
+ (NSString*)run
{
if (!windowController)
windowController = [MyPasswordWindowController new];
[windowController loadWindow];
windowController.passwordField.stringValue = #"";
if ([NSApp runModalForWindow:windowController.window])
return windowController.passwordField.stringValue;
return nil;
}
The application calls [MyPasswordWindowController run], so from the point of view of the user of this module it looks simple, but not so much when you look inside.
Set tags on your buttons to distinguish them. Have them both target the same action method:
- (IBAction) buttonAction:(NSButton*)sender
{
[NSApp stopModalWithCode:[sender tag]];
[self.window close];
}
Get rid of your _dialogCode instance variable and -windowWillClose: method.
-[NSApplication runModalForWindow:] will already center the window, so you can get rid of your -awakeFromNib method.
Get rid of the invocation of -[NSWindowController loadWindow]. That's an override point. You're not supposed to call it. The documentation is clear on that point. It will be called automatically when you request the window controller's -window.
Get rid of the static instance of MyPasswordWindowController. Just allocate a new one each time. There's no point in keeping the old one around and it can be troublesome to reuse windows.
I dont understand this yet. If I create an ivar of a class (no property). I set the ivar directly to for example #"patrik" in the - (void)viewDidLoad then the ivar value is still there when I click a button and checks for the Ivar value in the handler.
BUT!
If I have two buttons.
I set the ivar directly in the first button handler that I fire with a click. And then I check the Ivar in the second button handler that i fire. Then the ivar i empty?
Can someone explain what is happining?
Here is the code. Dead simple:
- (void)viewDidLoad
{
Ivar=#"Patrik";
[super viewDidLoad];
}
- (IBAction)secondButtonHandler: (id)sender{
NSString *result=Ivar;
}
- (IBAction)firstButtonHandler: (id)sender{
}
Here the Ivar is still here when I press the second button.
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (IBAction)secondButtonHandler: (id)sender{
NSString *result=Ivar;
}
- (IBAction)firstButtonHandler: (id)sender{
Ivar=#"Patrik";
}
But here its not after I have pressed the first button and then the second!?
I am not sure I am doing things the right way, or if I have it all hacked up.
I have a really simple test application (not document-based) I created for learning how to work with application-modal dialogs in Cocoa applications.
With the application project "TestModalDialog", I have a simple MainMenu.xib with the default view and a button, "Show Dialog", I added. I created a second XIB called TheDialog.xib that has "Cancel" and "OK" buttons. That xib has as its owner a class derived from NSWindowController called "TheDialogController"; the window outlet and delegate are connected to the controller.
Selection of "Show Dialog" on the main view will launch the dialog. Selection of "Cancel" or "OK" will dismiss the dialog. Here is the pretty simple code:
// TestModalDialogAppDelegate.h
// TestModalDialog
#import <Cocoa/Cocoa.h>
#class TheDialogController;
#interface TestModalDialogAppDelegate : NSObject <NSApplicationDelegate>
{
NSWindow *window;
TheDialogController* theDialogController;
}
#property (assign) IBOutlet NSWindow *window;
- (IBAction)showDialog:(id)sender;
#end
// TestModalDialogAppDelegate.m
// TestModalDialog
#import "TestModalDialogAppDelegate.h"
#import "TheDialogController.h"
#implementation TestModalDialogAppDelegate
#synthesize window;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
theDialogController= [[TheDialogController alloc] init];
}
- (void)dealloc
{
if(nil != theDialogController)
[theDialogController release];
[super dealloc];
}
- (IBAction)showDialog:(id)sender
{
if(nil == theDialogController)
{
NSAlert* alert= [NSAlert alertWithMessageText:#"Dialog Error" defaultButton:nil alternateButton:nil otherButton:nil informativeTextWithFormat:#"The dialog controller was not allocated."];
[alert runModal];
return;
}
NSInteger result= [NSApp runModalForWindow:[theDialogController window]];
// Do something with result....
}
#end
// TheDialogController.h
// TestModalDialog
#import <Cocoa/Cocoa.h>
#interface TheDialogController : NSWindowController
{
// BOOL userClickedCloseOrOk; // Removed based on answer.
// Should declare a common define - just being lazy.
NSInteger userClickedOk; // Added based on answer.
UInt16 timesShown;
}
- (IBAction)showWindow:(id)sender;
- (IBAction)closeDialog:(id)sender;
- (IBAction)okDialog:(id)sender;
//- (BOOL)windowShouldClose:(id)sender; // Removed based on answer.
- (void)windowWillClose:(NSNotification*)notification; // Added based on answer.
- (void)windowDidBecomeKey:(NSNotification*)notification; // To set title when modal.
#end
// TheDialogController.m
// TestModalDialog
#import "TheDialogController.h"
#implementation TheDialogController
- (id)init
{
self = [super initWithWindowNibName:#"TheDialog"];
userClickedOk= 0; // Added based on answer.
// userClickedCloseOrOk= FALSE; // Removed based on answer.
return self;
}
-(void)dealloc
{
// Do member cleanup if needed.
[super dealloc];
}
- (void)windowDidLoad
{
[super windowDidLoad];
// Initialize as needed....
[[self window] center]; // Center the window.
}
// Does not show with runModalForWindow.
- (IBAction)showWindow:(id)sender
{
// Just playing with the window title....
++timesShown;
NSString* newTitle= [NSString stringWithFormat:#"Shown %d Times", timesShown];
[[self window] setTitle:newTitle];
return [super showWindow:sender];
}
// This method no longer used for this solution based on the answer.
//- (BOOL)windowShouldClose:(id)sender
//{
// if(!userClickedCloseOrOk) // The user did not click one of our buttons.
// [NSApp abortModal];
// else
// userClickedCloseOrOk= FALSE; // Clear for next time.
//
// return TRUE;
//}
// Added based on answer.
- (void)windowWillClose:(NSNotification*)notification
{
[NSApp stopModalWithCode:userClickedOk];
userClickedOk= 0; // Reset for next time.
}
// Note - the title will update every time the window becomes key. To do the
// update only once per modal session, a flag can be added. There might be a better
// notification to catch.
- (void)windowDidBecomeKey:(NSNotification*)notification
{
++timesShown;
NSString* newTitle= [NSString stringWithFormat:#"Shown %d Times", timesShown];
[[self window] setTitle:newTitle];
}
- (IBAction)closeDialog:(id)sender
{
//userClickedCloseOrOk= TRUE; // Removed based on answer.
//[NSApp abortModal]; // Removed based on answer.
//[[self window] performClose:self]; // Removed based on answer.
[[self window] close]; // Know we want to close - based on answer.
}
- (IBAction)okDialog:(id)sender
{
userClickedOk= 1; // Added based on answer.
//userClickedCloseOrOk= TRUE; // Removed based on answer.
//[NSApp stopModal]; // Removed based on answer.
//[[self window] performClose:self]; // Removed based on answer.
[[self window] close]; // Know we want to close - based on answer.
}
#end
I had trouble with the modality - before I put in userClickedCloseOrOk and tests, if the user hit the close button (upper-left red dot), the dialog would close but the modal session was still running.
I realize I could just leave the close-button off the dialog to start with, but with it there, is what I have demonstrated a good way to catch that scenario, or is there a better way? Or, am I doing something wrong to start with, which creates that problem for me?
Any advice would be appreciated.
NOTE - Code from the original example commented out and replaced with code based on the answer. Also added a new notification handler.
In the okDialog: and closeDialog: methods call close instead of performClose: to avoid the window calling the windowShouldClose: delegate method. That way you don't need userClickedCloseOrOk.
Also I think you want to be calling stopModalWithCode: instead of stopModal since in the app delegate you seem to be interested in the result. And you can call stopModal or stopModalWithCode: instead of abortModal because you are always in the runloop when you call it (abort is for when you are outside of the modal runloop, like in another thread or timer's runloop).
In windowShouldClose: you are doing an action (abortModal) when you should only be answering the question "should this window close". The windowWillClose: delegate method is where you should do actions if you need to.
Sheets are useful when there is one window and it tells the user they can't do anything with it until they complete whatever is in the sheet. Application-modal windows are useful when you have multiple windows that the user can't interact with until they complete whatever is in the modal window or where there is an error that involves the whole application but is not tied to the content of one window. In their HIG Apple suggests avoiding the use of Application-modal windows whenever possible.
I have actually just been struggling with the same problem and found this link :
Stopping modal when window is closed (Cocoa)
Is it possible to somehow listen to, and catch, all the touch events occurring in an app?
The app I'm currently developing will be used in showrooms and information kiosks and I would therefore like to revert to the start section of the app if no touches has been received for a given couple of minutes. A sort of screensaver functionality, if you will. I'm planning to implement this by having a timer running in the background, which should be reset and restarted every time a touch event occurs somewhere in the app. But how can I listen to the touch events? Any ideas or suggestions?
You need a subclass of UIApplication (let's call it MyApplication).
You modify your main.m to use it:
return UIApplicationMain(argc, argv, #"MyApplication", #"MyApplicationDelegate");
And you override the method [MyApplication sendEvent:]:
- (void)sendEvent:(UIEvent*)event {
//handle the event (you will probably just reset a timer)
[super sendEvent:event];
}
A subclass of UIWindow could be used to do this, by overriding hitTest:. Then in the XIB of your main window, there is an object usually simply called Window. Click that, then on the right in the Utilities pane go to the Identities (Alt-Command-3). In the Class text field, enter the name of your UIWindow subclass.
MyWindow.h
#interface MyWindow : UIWindow
#end
MyWindow.m
#implementation MyWindow
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *res;
res = [super hitTest:point withEvent:event];
// Setup/reset your timer or whatever your want to do.
// This method will be called for every touch down,
// but not for subsequent events like swiping/dragging.
// Still, might be good enough if you want to measure
// in minutes.
return res;
}
#end
You can use a tap gesture recognizer for this. Subclass UITapGestureRecognizer and import <UIKit/UIGestureRecognizerSubclass.h>. This defines touchesBegan:, touchesMoved:, touchesEnded: and touchesCancelled:. Put your touch-handling code in the appropriate methods.
Instantiate the gesture recognizer in application:didFinishLaunchingWithOptions: and add it to UIWindow. Set cancelsTouchesInView to NO and it'll pass all touches through transparently.
Credit: this post.
Create a Class "VApplication" that extends from UIApplication
and paste these code to corresponding class
VApplication.h
#import <Foundation/Foundation.h>
// # of minutes before application times out
#define kApplicationTimeoutInMinutes 10
// Notification that gets sent when the timeout occurs
#define kApplicationDidTimeoutNotification #"ApplicationDidTimeout"
/**
* This is a subclass of UIApplication with the sendEvent: method
* overridden in order to catch all touch events.
*/
#interface VApplication : UIApplication
{
NSTimer *_idleTimer;
}
/**
* Resets the idle timer to its initial state. This method gets called
* every time there is a touch on the screen. It should also be called
* when the user correctly enters their pin to access the application.
*/
- (void)resetIdleTimer;
#end
VApplication.m
#import "VApplication.h"
#import "AppDelegate.h"
#implementation VApplication
- (void)sendEvent:(UIEvent *)event
{
[super sendEvent:event];
// Fire up the timer upon first event
if(!_idleTimer) {
[self resetIdleTimer];
}
// Check to see if there was a touch event
NSSet *allTouches = [event allTouches];
if ([allTouches count] > 0)
{
UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;
if (phase == UITouchPhaseBegan)
{
[self resetIdleTimer];
}
}
}
- (void)resetIdleTimer
{
if (_idleTimer)
{
[_idleTimer invalidate];
}
// Schedule a timer to fire in kApplicationTimeoutInMinutes * 60
// int timeout = [AppDelegate getInstance].m_iInactivityTime;
int timeout = 3;
_idleTimer = [NSTimer scheduledTimerWithTimeInterval:timeout
target:self
selector:#selector(idleTimerExceeded)
userInfo:nil
repeats:NO];
}
- (void)idleTimerExceeded
{
/* Post a notification so anyone who subscribes to it can be notified when
* the application times out */
[[NSNotificationCenter defaultCenter]
postNotificationName:kApplicationDidTimeoutNotification object:nil];
}
#end
Replace the Class name "VApplication" to our
Main.m
file like this
int main(int argc, char * argv[])
{
#autoreleasepool {
return UIApplicationMain(argc, argv, #"VApplication", NSStringFromClass([AppDelegate class]));
}
}
Register a notification for your corresponding view controller
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(applicationDidTimeout:) name:kApplicationDidTimeoutNotification object:nil];
And once the timeout occur the notification will fire and handle the event like this
- (void) applicationDidTimeout:(NSNotification *) notif //inactivity lead to any progress
{
}
You could put a transparent view at the top of the view hierarchy, and choose in that view whether to handle the touch events it receives or pass them through to lower views.
In Swift 4.2
1. Create subclass of UIApplication object and print user action:
import UIKit
class ANUIApplication: UIApplication {
override func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool {
print("FILE= \(NSStringFromSelector(action)) METHOD=\(String(describing: target!)) SENDER=\(String(describing: sender))")
return super.sendAction(action, to: target, from: sender, for: event)
}
}
In AppDelegate.swift file you will find application entry point #UIApplicationMain Comment that and Add new swift file main.swift
and add following code to main.swift file
import UIKit
UIApplicationMain(
CommandLine.argc, CommandLine.unsafeArgv,
NSStringFromClass(ANUIApplication.self), NSStringFromClass(AppDelegate.self))
ANUIApplication is class where we added action logs.
AppDelegate is default app delegate where we wrote application delegate methods.(Helpful for tracking action and file name in big project)
Is there anyway to detect the focus/unfocus of an NSDocument? I would like to dynamically update a menu item that pertains to the active document but I can't see any immediately obvious way of doing it.
The reason being, I'd like to activate and then populate the menu on document focus, and then unpopulate and deactivate on loss of focus.
Any ideas?
Thanks,
It appears the NSDocument is set as the delegate for all the document windows, and so the hooks required in my NSDocument subclass were:
- (void) windowDidBecomeMain: (NSNotification *) notification
{
NSLog(#"windowDidBecomeMain:");
}
- (void) windowDidResignMain: (NSNotification *) notification
{
NSLog(#"windowWillResign:");
}
- (void) windowWillClose: (NSNotification *) notification
{
NSLog(#"windowWillClose:");
}
The above solution didn't work, this did:
I have a window controller (subclassing NSWindowController) and I add a delegate on the window
-(void)windowDidLoad {
[super windowDidLoad];
self.window.delegate = self;
}
-(void)windowDidBecomeKey:(NSNotification *)notification {
// this gets called
}