I have a Document.xib and a MainMenu.xib, and a class MainController. I added an instance of MainController to Document.xib by dragging the NSObject from the object library to Document.xib's instance tree and setting the class in the property to MainController. I added a button and connected it to one of the actions provided by MainController.
So far, so clear. Now I basically want to call the same action from a menu item. Obviously, I can't just add another instance of MainController to the MainMenu.xib, because I'd end up with two instances. There should be just one per document, and the menu item should call the action in the active document's MainController. How do I do this?
This is what the First Responder proxy icon is for. You can connect menu items to the First Responder proxy, and messages will be sent up the responder chain until they reach an object that handles the message. Your document, along with views and other objects, will participate in the responder chain, and so will have an opportunity to handle the message provided an object earlier in the chain hasn't already done so. The responder chain concept also ensures that the message is delivered to the active document -- if you have more than one document open, you naturally want menu commands to be handled by the document that the user is working on.
So, just make the First Responder icon the target for your menu items and the right thing will happen.
Related
I have a document based application. I have just created menu items in the storyboard and IBActions in my view controller. However the usual way I connect an action to a target doesn't work
-(IBAction) markAsHidden:(id)sender;
-(IBAction) markAsVisible:(id)sender;
-(IBAction) toggleHidden:(id)sender;
Here is what I see when from my menu item I press Ctrl and mouse click from menu to View Controller. It does not show my IBActions.
Any idea ? My 2 cents guess is that it has to do with the app being document based but... not really sure
Connect the menu items to the application scene's First Responder. When you connect to the application scene's First Responder, your view controller's IBActions should appear in the HUD's list of available actions instead of the action segues shown in your screenshot's HUD.
Why can't I connect my menu to my view controller IBAction?
Because your menu items and view controller are in different scenes in the storyboard. You can think of a scene as an independent graph of objects that are instantiated when the scene is loaded from the storyboard. Objects in different scenes can't be connected together in the storyboard because they're not loaded at the same time.
Just for fun, try creating an instance of your view controller in the Application Scene in your storyboard. To do that, you'll probably need to drag a plain old NSObject instance into the scene and then set its type. Once you do that, you'll find that you can drag a connection from a menu item to that view controller just as you'd expect, but you can't drag a connection to a different object of the very same type in a different scene.
Note: Once you've played around enough to convince yourself that it works, remember to delete the view controller that you added. A view controller without a view is like a duck without a quack, and a view controller and its view hierarchy should be in their own scene.
My 2 cents guess is that it has to do with the app being document based
No, it doesn't have anything to do with that. You'd have the same problem in an app that's not document-based. You'd also have the same problem if your app were .xib-based instead of using storyboards, since the controller you'd be trying to connect to would be in a completely different .xib file.
The easy solution, as Mark already described, is to use the responder chain. The First Responder proxy object is part of every scene, so you can always make connections to it. When you connect a menu item to First Responder its target will be nil, which tells NSMenu to walk the responder chain until it finds an object that responds to the menu item's action message. It then sends the message to that object.
If you are converting a project from objective C to Swift, do not make my mistake. When writing your IBAction write like this:
#IBAction func someAction(_ sender:AnyObject) {
// this will work
}
Do not omit the underscore before sender or the Interface Builder won't be able to connect to your action as in here:
#IBAction func someAction(sender:AnyObject) {
// this won't work and IB won't connect to this action
// because sender will be part of the symbol name
}
I'm trying to capture all the NSTextFinderClient calls on my custom NSTextView subclass.
The show action is called on my -(void)performTextFinderAction:(id)sender override, but for find next, find previous, etc. it's not called.
Any ideas?
Thanks!
Edit:
If you create a new project and drag an NSTextView from interface builder, command-g and command-shift-g (find next and find previous) don't work when the find bar is first responder.
Why is this?
I need a custom subclass of NSTextView to respond to the find bar for every event.
I searched in the Apple's TextEdit source code because with TextEdit, the standard search bar within the Text View works fine for command-G (and other shortcuts) even the search field is the first responder.
I found the solution.
Go to your nib for the main menu, and select the "Find" (and related) menu items. They should be bound to the default action called "performFindPanelAction:." Now unbind them and bind to "performTextFinderAction:" of the First Responder instead.
You may not find that action in the First Responder's action list. So you need to add it by yourself in the First Responder's attributes inspector pane.
This was meant by the document below saying
Before OS X v10.7, the default action for these menu items was performFindPanelAction:. Whenever possible which you should update your implementation to use this new action.
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSResponder_Class/#//apple_ref/occ/instm/NSResponder/performTextFinderAction:
The find bar communicates privately with the client's NSTextFinder instead of calling NSResponder's -performTextFinderAction:. This is necessary to allow find to work when something besides the client has key focus.
What are you trying to accomplish?
I have a menu item whose state should depend on whichever NSDocument is open. From my understanding, to make its state change dynamically I should use the NSMenu delegate method menuNeedsUpdate:.
It seems like I would want to have the menu's delegate be the First Responder in MainMenu.xib. However, Interface Builder won't let me set it as the Main Menu's delegate. How can I make a delegate which will be able to access the currently active document?
I generally make such changes in the validateMenuItem: method being called before the menu is shown. The receiver of the action is asked whether the item is to be enabled or not. But you can do pretty much any change there. Since 10.5 it is also safe to add and remove items during such a call.
I have a document based cocoa application with an item in the application menu hooked up to an IBAction. Clicking the item needs to perform a task that uses an IBOutlet in the main nib file which is using another class, MyDocument. Creating 2 objects of the same class, one in each nib seems to not be working. How can I access the outlet?
Actions for menu items are often sent to the first responder so that whatever is currently selected can act on it.
It sounds like this action is something that works on the current document, then it should be implemented by the document. In this case have the menu send it's action to the first responder and then put the action method in the MyDocument class.
If the action you are trying to send is a custom one: in the Main Menu nib select the First Responder item, add your method name, then connect the menu item's selector to the action.
Read the Responders section of the Cocoa Event-Handling Guide for more info.
To summarize the above, in your NIB/XIB file, in interface builder make the connection to the First Responder object, not to Files Owner or anything else. You'll still be offered a lit of actions across potential first responders.
Cocoa then takes that selector and looks for it, starting with the NSView (if any) that's currently the first responder, then with the NSDocument that's currently in use, then with it's window controller etc etc all the way up to the Application delegate. The first object it checks that actually implements that method, it will use that object (after validating it with that same object).
So:
#interface MyDocumentTypeA : NSDocument {
}
-(void)myMenuAction:(id)sender;
-
#interface MyDocumntTypeB : NSDocument {
}
// -myMenuAction: not implemented here
-
#interface MyApplicationDelegate ... {
}
-(void)myMenuAction:(id)sender;
-
In Interface builder (or even programmatically), if you've linked the "action" of the menu item to a selector named "myMenuAction:" on the First Responder (which equates to not specifying a target when done programmatically), for the above two document subclasses the following will happen.
For MyDocumentTypeA, when the user selects that menu item, MyDocumentTypeA's -myMenuAction: will be invoked. Since MyDocumentTypeB does not implement this action, Cocoa will continue to look up the responder chain until it gets to your application delegate, which does implement it, so it will be invoked here instead.
If Cocoa finds no objects in the responder chain that implement the method, the menu item remains disabled.
There is a way how to do this, I've posted the answer in a similar thread: Access IBOutlet from other class (ObjC)
I have a table view that gets refreshed two different ways. Both are through a button, and as a matter of fact, both are through the same IBAction in the same class!
Here's my problem:
The buttons are in two different .xib files, the button in the same xib as the table view works perfectly, while the one in the different xib does the method to get the new data, but it DOES NOT refresh the table. Same exact method, different results. To get the IBAction for the other button I simply dragged out an NSObject in IB and set its class to the class of my table view, which contains the IBAction, then hooked it up to my button.
How can I fix this?
Sounds like you're creating a second, parallel, object of your class in the second XIB. The button sends a message to that instance, which does some of the stuff you expect because it's an object of the right class, but it isn't actually the right object and isn't connected to your view.
What you need to do is ensure that both buttons talk to the same instance. This is easiest if the target is in the responder chain -- you should be able to set the button's target to First Responder and the message will find its way to the right place. Otherwise, you need to get a pointer to the target into the XIB, eg as an IBOutlet in the object that will be File's Owner.