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.
Related
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?
As shown from the diagram, I am trying to have two viewControllers connected to the viewController on the right that serves as a menu between the viewControllers.
When the app is first run, it will load the first view controller on top. A button will show the menu modally and depending on which button is pressed (I am planning to add more) the corresponding viewController will be shown. The FIRST time that VC2 is pressed, it will load the second viewController, but when the menu is called again and VC2 is pressed, I want it to unwind instead of reload the view controller.
I have managed to unwind to the first viewController but when trying to unwind to the second viewController, the action is ignored. Your help will be greatly appreciated. Thank you.
You cannot unwind to a viewcontroller that has not been previously presented, because the desired destination VC will not be held in the navigation controller stack. Unwind segues are designed to 'pop' several steps back in a navigation history. In your scenario, the only possible pop available is to VC1.
You might also want to review your 'menu' concept, considering that UINavigationController objects can provide this behaviour with little fuss. You also don't need two separate UINavigationController objects to achieve what you want.
To do this, simply connect your menu VC as the root view controller for the UINavigationController, then connect the menu buttons to VC1 & VC2. Then you'll be able to move between the three screens with ease (see below for example).
I started learning Objective C and programming late last year and I’m dealing with Core Data, so I need some help.
I’m trying to select an object in VC3 and pass it to VC1 when the ‘Select’ button is pressed. Once pressed then it pops back to VC1 and display the object in the cell. I don’t think I can use a segue, because I want to go back and not forward. Also, I'm not going back to the root view controller.
Everything else works with segues, protocols and delegates except this issue. I’ve tried delegates, but they don’t seem to work, perhaps because there’s a view controller in between them or something. However, I’m very new to programming so I could be wrong.
I would appreciate some help and guidance.
The modern way (ios6+) to do this is with an unwind segue
You create one of these by defining a method that takes a single argument of a storyBoardSegue.
- (IBAction)unwindOnSelect:(UIStoryboardSegue *)segue;
Add this method to the viewController you want to unwind to.
Then, in the storyboard scene you want to unwind from... CTRL-drag from your 'Select' button to the green 'exit' symbol in the bar at the bottom of the scene. The unwind segue should appear in the popup as an option. Select it. Your button will unwind you to that destination, and code in the unwind segue method will be executed.
The segue object holds a reference to both source and destination viewControllers, so that is how you can get the data from one to the other.
- (void)unwindOnSelect:(UIStoryboardSegue *)segue
{
self.myObject = segue.sourceViewController.myProperty;
}
You will probably get an error here, "no known instance method for selector "myProperty". One way round this is to #include "viewController3" at the top of VC1's .h or .m file. You may have to typecast as well:
self.myObject = [(ViewController1*)segue.sourceViewController myProperty];
Another way is a bit of indirection:
if ([segue.sourceViewController respondsToSelector:#selector(myProperty)]) {
self.myObject = [segue.sourceViewController performSelector:#selector(myProperty)];
}
This has the benefit that you do not have to #include "viewController3", so you are a bit more decoupled, a bit more Objectified.
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!")
}
i am working on storyboards which has couple of views on first view a condition is placed i want if the condition satisfies then only navigation should happen
For this i have used Custom segue but no matter my condition satisfies or not it navigates to new view.
I have created method in custom segue class
- (void) perform{
NSLog(#"source %#",self.sourceViewController);
NSLog(#"dest %#",self.destinationViewController);
UIViewController *sVC=self.sourceViewController;
UIViewController *dVC=self.destinationViewController;
[sVC.navigationController pushViewController:dVC animated:YES];
}
I want to set condition if result is 1 then only it should navigate. Woul prepareforsegue or initwithsegue provide me any help
Are you saying that you only want to perform the segue if a condition is true?
If so, instead of creating the segue directly from a control or table cell, create a triggerless segue. A triggerless segue has the view controller as its source, and it won't ever fire automatically. Instead you can fire it programmatically any time you like, including from an IBAction.
To create a triggerless segue, start control+dragging the segue from the containing view controller icon in the scene dock at the bottom of the scene. Drag to the destination scene like normal, and pick the segue type. Select the segue, and in the inspector, choose a segue identifier.
At runtime, when you want to perform the segue, invoke -[UIViewController performSegueWithIdentifier:sender:]. You can pass any object you'd like for the sender, including nil. If the sender has no use to you, pass nil.
So, in summary:
Create a triggerless segue from the view controller to the destination scene
Set an identifier for the segue in the inspector
At runtime, and form code, call -[UIViewController performSegueWithIdentifier:sender:] when you want to trigger the segue.