Execute IBAction on a custom event - cocoa

How would I be able to run an IBAction based on another event? For example I could use a piece of code to run an IBAction.
What I'm trying to do is have two different buttons run one or more IBActions.

You can hook multiple buttons up to the same IBAction, so you could do it that way.
But IBActions are just methods. For example, if you have some action doSomething:, you can just call it on an object:
[obj doSomething:nil];

An IBAction is just a hint for the Interface Builder. It's actually another way of saying void.
#define IBAction void
So there is nothing special about it.
In Interface Builder, you can connect touch event for different buttons to the same IBAction.
You can also call IBAction methods from another IBAction method. Use the sender argument to identify the source of the event.
For example,
-(IBAction)buttonTapped:(id)sender {
UIButton *btn = (UIButton *)sender;
NSLog(#"tapped: %#", btn.titleLabel.text);
[self anotherIBAction:sender];
}

Related

OS X storyboards: using "show" segue without allowing duplicate new windows to show?

Right now I have an OS X storyboard app that has a main window, and a button on it that triggers a "show" segue for another view controller. Right now I've got the segue set up as modal because if I don't, the user could click the same button again and end up with two copies of the same window.
Is there a way for me to accomplish this without having to restructure the storyboard to embed these view controllers in a separate window controller (which would seem to defeat the purpose of the flexibility the storyboards offer)?
Edit: While the answer below does work, it is definitely not the best way. In your storyboard, select the view controller for the destination view then go to the attributes inspector and change Presentation from Multiple to Single. That's it, no code required.
Not sure this is the best way but in the NSViewController that is pushing the segue, you could add a property for the destination NSViewController and, in your prepareForSegue:sender: method, assign the destination view controller. Finally, in the shouldPerformSegueWithIdentifier:sender: method, check to see if the destination view controller is assigned, and, if so, bring its window to the front and return NO meaning don't perform the segue, otherwise, return YES. Here's a quick example (to be included in the NSViewController with the button to initiate the segue):
#interface ViewController ()
#property (weak) NSViewController *pushedViewController;
#end
#implementation ViewController
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if (self.pushedViewController) {
[self.pushedViewController.view.window makeKeyAndOrderFront:self];
return NO;
}
return YES;
}
- (void)prepareForSegue:(NSStoryboardSegue *)segue sender:(id)sender {
self.pushedViewController = segue.destinationController;
}
#end
When you close the window containing the destination view controller, this will set the pushedViewController property of the original view controller to nil so the segue will perform if the window is not already opened. Again, there may be a better way to do this. Hope this helps.
Jon

How to make a custom NSView within a NSMenuItem becomeFirstResponder?

I have a custom NSView within a NSMenuItem (attached to a MenuBar) that respond to a mouseDown event. But I need to click twice on the custom view for the mouseDown function to be called, this is because the custom view should be first responder. And when I override the method acceptsFirstResponder in my CustomView Controller as indicated by the Cocoa Event Handling Guide, it does not work. What is the solution? Is it doable?
Override the NSView method acceptsFirstMouse: to return YES for the event in question. If you only want to accept the first mouse click for some types of events, you can do that by examining the event parameter passed in. Unless there is something special about the NSMenuItem case in particular, this should be what you want; it's the standard Cocoa mechanism for this. Note that this method is not the same as the acceptsFirstResponder method you have tried. See Apple's doc for details.
For reference I have just added to my custom view the following:
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent {
return YES;
}

IBAction not triggering NSView drawing

I'm missing something fundamental about NSView. I have a Cocoa Application with an Objective C class named DataSource that is just a regular class, it's not in the nib. The data source has a single instance variable, an NSColor *, and it has a getter and setter.
The view class instantiates the DataSource in awakeFromNib:
- (void)awakeFromNib{
NSLog(#"awakeFromNib");
ds = [[DataSource alloc] init];
}
and then queries the DataSource for the color to use in drawRect. It works fine. I also implement
- (void)mouseDown:(NSEvent *) anEvent;
in the view class, change the color of the DataSource, and then call
[self setNeedsDisplay:YES];
and it also works as I expect when I click in the custom view.
But if I hook up a button in the nib, wired to this IBAction in the view class:
- (IBAction)buttonPushed:(id) sender {
NSLog(#"buttonPushed");
[ds setData:[NSColor cyanColor]];
[self setNeedsDisplay:YES];
}
the data source updates, but drawRect is never called, despite setNeedsDisplay. In my more complicated version, if I click in the view (in a way that doesn't change the color), I will then get the update (caused by the button). Something is delaying drawing. How can I fix this?
Update: There is no controller and there are no outlets. The NSView subclass contains buttonPushed. The data source updates immediately upon button push, but drawing is delayed, despite calling setNeedsDisplay:YES from the view class. Drawing is delayed indefinitely, unless something else happens to trigger it.
Where is the IBAction located? Are you using some view controller? Is the NSView an outlet in that controller?

NSWindow tracking

I would like to track each time a certain window appears (becomes visible to the user) in a OS X app. Where would be the most adequate place to call the tracker?
windowWillLoad, maybe?
I expected to find something like windowWillAppear but it seems I'm thinking too much iOS.
How about getting notification such as NSWindowDidBecomeMainNotification, By main I guess the one which is top most on screen directly visible by user.
see : Apple Documentation
Yes, one would expect that a window would notify its delegate or its controller with a windowWillAppear or windowDidAppear message, or post a documented notification like NSWindowDidAppearNotification. But alas, none of those exist. I filed a bug report with Apple and was given the advice to use a storyboard and a view controller instead. This is unhelpful in legacy apps that already use a bunch of window controllers and xibs.
You could subclass NSWindow and override orderWindow:relativeTo: to send a notification. Most, but not quite all, of the messages that make a window show itself ultimately go through this method, including orderBack:, orderFront:, makeKeyAndOrderFront:, and -[NSWindowController showWindow:]. But orderFrontRegardless does not go through orderWindow:relativeTo:, so you would also want to override that for completeness.
Another way to be notified is to make a subclass of NSViewController that controls some view that's always visible in the window. The view controller will receive viewWillAppear and viewDidAppear.
If you're subclassing NSWindow or NSViewController already for some other reason, either of these is a reasonable solution.
If you're not subclassing NSWindow already, and don't have an NSViewController subclass for a view that's always visible in the window, then another way is to use Cocoa bindings to connect the window's visible binding to a property one of your objects. For example, I have a custom NSWindowController subclass. I gave it a windowIsVisible property:
#interface MyWindowController ()
#property (nonatomic) BOOL windowIsVisible;
#end
and I implemented the accessors like this:
- (BOOL)windowIsVisible { return self.window.visible; }
- (void)setWindowIsVisible:(BOOL)windowIsVisible {
NSLog(#"window %# became %s", self.window, windowIsVisible ? "visible" : "hidden");
}
and in awakeFromNib, I bind the window's visible binding to the property like this:
- (void)awakeFromNib {
[super awakeFromNib];
[self.window bind:NSVisibleBinding toObject:self withKeyPath:NSStringFromSelector(#selector(windowIsVisible)) options:nil];
}
When the window becomes visible, the setWindowIsVisible: setter is called with an argument of YES. Note that if the whole app is hidden and reappears, the setter is called again, even though it wasn't called with argument NO when the app was hidden. So be careful not to assume the window was previously hidden.
Also, the binding might create a retain cycle, so you should probably unbind it when the window is closed, unless you want to keep the window and controller around. Note that the window does post NSWindowWillCloseNotification when it's closing, so you don't need any special magic to detect that.

Call IBAction in different class

My document based application has a window with a tableview. The tableview has a datasource which points to a class of type NSObject (called HopBill) which includes a NSMutableArray (aHopBill) and the needed tableview methods. So far so good.
For adding rows to the tableview I've added a sheet which is controlled from a NSWindowController (called HopBillSheetController). When pressing the OK button in the sheet. I actually need to do two IBActions (which is not possible): Add the row to the array of the tableview and close the sheet. I can connect the OK button in the sheet to the NSWindowController (to close the sheet) or connect it to the NSObject (to add the row to the array). But I want both :-)
Is it possible to call the IBAction in the NSWindowController from the NSObject? Or is there another way to do this?
I'm quite a beginner to Cocao and Objective-C, so please be gentle :-)
If your sheet is a nib/xib with an NSPanel, the call to close it is simply [panel close]; Assuming your window controller has a property for the panel, you can put the close code at the end of its row-adding IBAction. Or you could have the IBAction itself call another method if you prefer.
If your panel is running modal, you might need to stopModal too. (That's what's needed if everything stays frozen after the panel closes; otherwise never mind.)
Assuming hopBill, your data source, is a property of the window controller, any IBAction you write in the window controller also has access to hopBill; it can do everything you need.
So add a single IBAction to the window controller and connect the panel's OK button to it. That ought to work.
As for calling an IBAction from somewhere other than a control in a nib, yes, you can do that. Use a reference to the control as the sender arg, or nil if the IBAction doesn't use the sender arg.
You could also create your panel programmatically, or use NSAlert. But it sounds like your current setup is simpler -- and therefore better.
Take a look at this h file for an app controller: Apple's ClockControl example
The NSMutableArray *appointments property is the actual data source that will be used by the NSTableViewDataSource protocol methods. The IBAction "addAppointment" can access "appointments" directly: [self.appointments addObject:whatever atIndex:whatever];
The ClockControl example could be modified to use HopBill. You would import its declarations up top: #import "HopBill.h" And then instead of the "appointments" property, it would declare HopBill *hopBill; And "addApointment" would access HopBill's mutable array (aHopBill) like this: [self.hopBill.aHopBill addObject:whatever atIndex:whatever];
Why you can’t send messages to hopBill:
First, because although you declare it, you never initialize it. You have:
HopBill *hopBill;
[self.hopBill.aHopBill addObject: bHopAdditionAtInit];
It should be:
HopBill *hopBill = [[HopBill alloc] init];
[hopBill.aHopBill addObject: bHopAdditionAtInit]; // “self” won’t work here
Second, you’re declaring it inside an IBAction method, (doneHopBillSheet:), so it’s a local variable, accessible only within that method. If HopBill is holding your table’s data source cache, it should be a property of the controller which implements the NSTableViewDataSourceProtocol methods.
In your HopBill interface, you declare the aHopBill array to be a property, and you initialize it in HopBill’s init method (you should also release it in HopBill’s dealloc method). You need to do the same thing for the controller — it should have an instance of HopBill as a property, and that instance should be initialized in the controller’s init method.
If you want HopBillController to manage the tableview, its interface declaration should look like this:
#interface HopBillSheetController : NSWindowController <NSTableViewDelegate, NSTableViewDataSource> {
…
}
And, then, of course, you have to implement the relevant NSTableViewDelegate and NSTableViewDataSource methods.
Also, the controller must have an IBOutlet property for the tableview itself, and in the controller’s awakeFromNib method, it has to assign itself as delegate and datasource:
[self.tableview setDelegate:self];
[self.tableview setDataSource:self];
(The self-dot syntax assumes you’ve set up #property and #synthesize code for tableview.)
The IBAction method that adds items to your table must be in that controller class, or in a class that has a property which is an instance of the controller class. Then the IBAction method will have access to the aHopBill array and can add the new object to the array, after which it will call [tableView reloadData], which will in turn trigger the tableview protocol methods and update the table.
Now, that means that the xib containing the tableview has to have the controller as its file’s owner. Since you’re using NSDocument, I suspect that, instead, you would put the tableview outlet in the NSDocument subclass. And you would give that doc subclass a property which is an instance of the controller. The IBAction methods would also be in the doc subclass, and so they would have access to the controller and its HopBill property. Or maybe you would simply make the doc subclass the controller, rather than using the separate HopBillSheetController class. I’m not sure about the NSDocument stuff. But, remember, the IBAction method can itself call other methods, as long as it has access to instances of the classes in which those methods are declared.
Apple has an example using both the tableview delegate and datasource protocol methods. Go to this link and download the sample code: tableview example
It looks like a nice app. Good luck.

Resources