How do I set the initial first responder per view in a cocoa app that switches between different views? - cocoa

My Cocoa App uses one ViewController. I do not use the InterfaceBuilder On app launch a view will be created and the user can do stuff. When clicking a specific button the VC (as the view's delegate) receives a message and then replaces the view with another.
In this new view I want a specific UI element to be the first responder. So far I have not been successful.
The new view has a reference to the desired element (a subview), so the VC can pass it to the window's makeFirstResponder(:_) method.
I tried to do that in the following places:
at the end of the view's init
in the view controller's viewWillAppear()
in the VCs viewDidAppear()
in the latter two I tried:
if let myView = self.view as? MyView {
... here I try to set the UI element as firstResponder ...
}
But in any case I get the following Message:
[General] ERROR: Setting <NSTableView: 0x7f8c1f840600> as the first responder for window <NSWindow: 0x7f8c1ef0efc0>, but it is in a different window ((null))! This would eventually crash when the view is freed. The first responder will be set to nil.
So it appears that at the time I try to set the firstResponder the new view has not yet been attached to the window.
What I also tried is to override the MyView's becomeFirstResponder()method, assuming that when the view is finally presented in the window it will receive that command, but unfortunately this method does not get called.
Is there an easy way to specify an entry point for the responder chain / key view loop per view?

Related

Perform segue after unwinding

I have a main screen wrapped in a navigation controller.
The main screen has several buttons that trigger segues to other views. One button goes to a table view.
A selection in this table should trigger a segue that is normally performed by one of the buttons on the main view.
I was assuming that I need to unwind to the main screen first, and then trigger a segue from the unwind segue programmatically, but what happens when I do that is that it performs the programmatic segue first, then unwinds, and ends up on the main screen again.
What is the correct way to deal with this situation? I don't want to be able to go back to the table view after the programmatically called segue, the back button should then go to the main view.
In case it helps to explain my use case a little more: The table view is a list of levels. A selection should launch my game view with the level picked in the table view.
My unwind segue in my main view:
#IBAction func backFromLevelSelectionUnwindSegue(segue:UIStoryboardSegue) {
performSegueWithIdentifier("playSegue", sender: self)
}
obviously playSegue is the segue to the game view controller.
An answer to a similar question suggests setting a boolean flag and then performing the segue in viewDidAppear, but it seems like that viewDidAppear should not have to know about an unwind segue that has occurred. Is there a "correct" solution that I haven't come across?
In your backFromLevelSelectionUnwindSegue, your are still in a unwind segue context. So call performSegueWithIdentifier after the context finished using dispatch_async or performSelector:withObject:afterDelay like below.
#IBAction func
backFromLevelSelectionUnwindSegue( segue:UIStoryboardSegue ) {
dispatch_async( dispatch_get_main_queue() ) {
self.performSegueWithIdentifier( "playSegue", sender: self )
}
}
Would it be possible for you to perform a segue to the game view without unwinding it to the main screen? Then when you unwind the segue, unwind it to the main view instead of back to table view? If I remember correctly, we are able to unwind past the presenting view controller.
This is a matter of opinion, but in your case I think you should simply have a segue between the tableview and the game, with any extra data necessary being passed to the tableview VC from the main VC.
If you need go back multiple viewControllers in a UINavigationVC, I would look at using popToRootViewController or use an unWind between them. e.g. call unwind from the 3rd viewController, with the handler in the 1st viewController
It's possible to have multiple unwind segues that go back to different places. In the scenario given the view controllers should be arranged Main->LevelSelect->Game
and then when game ends you have buttons for 2 or 3 unwind segues. First one is exitToGameStart that allows the player to restart the same level. exitToLevelSelect allows player to choose a new level. And optionally exitToMainMenu goes all the way back to the start. For a full example, see Apple's UnwindSegue sample, and in particular the "Start Over" button on the last table that performs the exitToQuizStart unwind segue and the "Return To The Menu Screen" that performs the exitToHomeScreen unwind segue. Code for the receiving methods are below:
QuestionViewController.m
//! Unwinds from the ResultsViewController back to the first
//! QuestionViewController when the user taps the 'Start Over' button.
//
// This is an unwind action. Note that the sender parameter is a
// 'UIStoryboardSegue*' instead of the usual 'id'. Like all unwind actions,
// this method is invoked early in the unwind process, before the visual
// transition. Note that the receiver of this method is the
// destinationViewController of the segue. Your view controller should use
// this callback to update its UI before it is redisplayed.
//
- (IBAction)exitToQuizStart:(UIStoryboardSegue *)sender
{
// The user has restarted the quiz.
[self.currentQuiz resetQuiz];
}
MainMenuViewController.m
//! Unwinds from the ResultsViewController back to the MainMenuViewController
//! when the user taps the 'Return to the Home Screen' button.
//
// This is an unwind action. Note that the sender parameter is a
// 'UIStoryboardSegue*' instead of the usual 'id'. Like all unwind actions,
// this method is invoked early in the unwind process, before the visual
// transition. Note that the receiver of this method is the
// destinationViewController of the segue. Your view controller should use
// this callback to retrieve information from the sourceViewController. Used
// properly, this method can replace existing delegation techniques for
// passing information from a detail view controller to a previous view
// controller in the navigation hierarchy.
//
- (IBAction)exitToHomeScreen:(UIStoryboardSegue *)unwindSegue
{
// Retrieve the score from the ResultsViewController and update the high
// score.
ResultsViewController *resultVC = (ResultsViewController*)unwindSegue.sourceViewController;
self.highScore = MAX(resultVC.currentQuiz.percentageScore, self.highScore);
}
Note: to build the old project in the latest Xcode, open the storyboard, file inspector, builds for, pick a newer iOS version.

NSWindowController/NSViewController "Presentation" setting in Storyboard

What exactly does the Presentation option(in Attribute Inspector) do in StoryBoard for Cocoa.
It gives two options to select from
Single
Multiple
P.S When googled the title, results are related to powerpoint presentation
The presentation style affects "Show" segues. Possibly it affects other segues too, but I only tested a Show segue. I tested on OS X 10.10.5 (Yosemite) with Xcode 7.1.1.
If a window controller's presentation style is "Multiple" (the default), then a Show segue to the window controller always loads a new instance of the window controller from the storyboard. This means that you can end up with multiple instances of the window controller at once, each with its own window on the screen. By default those windows will stack on top of each other, so it won't be obvious what happened until you move or close one.
If a window controller's presentation style is "Single", and an instance of the window controller has already been loaded from the storyboard, and that window controller still exists (presumably because its window is still on screen), then a Show segue to that view controller will not create a new instance. Instead, the Show segue will bring the existing window controller's window to the front.
This behavior is useful if you want behavior like, say, Xcode's Devices window, where there can only be one such window. You create a "Devices" menu item in the Window menu in your storyboard, and connect it to the Devices window controller in the storyboard with a Show segue. Set the Devices window controller's presentation style to Single. Now the menu item will never create a second Devices window controller if one already exists.
You'll probably want to somehow set the window's excludedFromWindowsMenu property to true, so it doesn't appear twice in the Window menu (because by default it appends itself to that menu). You could, for example, use a subclass of NSWindowController that sets it:
class DevicesWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
window?.excludedFromWindowsMenu = true
}
}
View controllers also have a presentation style, because you can also connect Show segues to view controllers. A Show segue connected to a view controller automatically creates a window controller to contain the view controller at runtime. The window controller's presentation style is effectively set to the view controller's, so you get the same singleton behavior if you set the view controller's presentation to Single.
As far as I can tell, the storyboard setting has no corresponding public property or method you can use in code.
If you connect the Show segue to a storyboard reference (new in Xcode 7), then the segue ignores the presentation style of the destination window controller, and acts as if it were "Multiple". This happens even if the destination is actually in the same storyboard as the reference.

Why can't I connect my menu to my view controller IBAction?

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
}

Does anyone know what the new Exit icon is used for when editing storyboards using Xcode 4.5?

Right-clicking the Exit icon yields an empty window. Can't Ctrl-drag a connection to any IB elements or corresponding source files. Docs give no love. Doesn't appear in nib files, only storyboards. My assumption is that it's a corollary to segues, but I don't see any new methods to back it up. Anyone?
I had a hard time following the accepted answer so here is more detail.
Given the photo below on view controller C you can "exit" back to any view controller in the segue path.
ViewController A you can write:
- (IBAction)done:(UIStoryboardSegue *)segue {
// Optional place to read data from closing controller
}
ViewController B you can write:
- (IBAction)back:(UIStoryboardSegue *)segue {
// Optional place to read data from closing controller
}
ViewController C you control drag from "back" button to the green exit option and select back:
ViewController C you control drag from "done" button to the green exit option and select done:
Note: Even though the methods are on other view controllers they show up for the ViewController C's exit. Control dragging and selecting a method defines which ViewController to unwind to.
There's a lot of information in the WWDC video "Session 407 - Adopting Storyboards in your App."
Say you have two view controllers linked by a segue. Implement the following exit action on the first view controller:
- (IBAction)done:(UIStoryboardSegue *)segue {
NSLog(#"Popping back to this view controller!");
// reset UI elements etc here
}
Then, on Storyboard scene for the second view controller, Ctrl-drag from a UI element, such as a button, to the exit icon at the bottom of this view controller. The done: action you added to the code of the first controller will appear as an option. Now, activating the button you Ctrl-dragged to the exit icon will pop back to the first view controller and maintain its original state (ie UI elements such as text input supposedly still intact).
As addition to Eric answer here is how it works with swift:
The function you add to the destination controller looks like:
#IBAction func backFromOtherController(segue: UIStoryboardSegue) {
NSLog("I'm back from other controller!")
}

Receive window notifications

I have an NSWindow set up in Interface Builder. I have set the class of File's Owner to my NSWindowController and linked the window property of the controller to my NSWindow.
My controller implements NSWindowDelegate.
Now, in my controller, I have added the following:
- (void)windowDidLoad
{
[super windowDidLoad];
[self.window setDelegate:self];
}
- (void)windowDidBecomeMain:(NSNotification *)notification
{
NSLog(#"Did become main.");
}
Still, -windowDidBecomeMain: isn't called. Does anyone know why this is?
EDIT:
Trying to show a window from AppDelegate on launch. The main nib (declared in Info.plist) contains a menu item only which is linked to the AppDelegate. In the application delegate, I show an icon on the status bar and when this icon is clicked, I display the menu from the main nib.
In the application delegate, I also want to display a window which should have a window controller assigned to take care of the logic.
I believe that when this works, I will receive my window notifications.
Now, the following code doesn't show the window and I can't figure out why.
DemoWindowController *dwc = [[DemoWindowController alloc] initWithWindowNibName:#"DemoWindowController"];
[dwc showWindow:self];
Note that self is the application delegate.
I suspect your problem is due to the fact that your window controller is not actually the object that is the nibs file owner.
When you change the class in interface builder you are telling it what outlets and actions are available (which is why you are able to drag to the window outlet) but you are still responsible for passing in this object yourself.
In the case of a non-document based application, you will have a main method which calls NSApplicationMain. What this does is basically look up and load the window nib that is specified in your info.plist file and pass the current NSApplication instance to this nib as the files owner (so even though you changed the class type to NSWindowController, the object being passed in is actually of type NSApplication).
The easiest way to fix your problem is to get rid of your window controller for now (as it isn't actually doing anything yet).
You should implement the -windowDidBecomeMain: method in your app delegate. Then Ctrl+drag from your window to your appDelegate to set it as the delegate of the window to get your notifications.
Update
To answer your question regarding the WindowController beware of the following two issues:
You are creating your window controller variable (dwc) in your applicationDidFinishLaunching: method. This is released the moment you leave the method taking your window with it. Create an instance variable to hold onto the window controller instead.
Ensure that your second window nib has its file owner set to NSWindowController (or your window controller type) and that its window outlet is connected to the window in the nib file.
Your window should now display.

Resources