I use a push segue to transition from a uisearchcontroller located within my root view controller, to a second view controller. When I try to use an unwind segue method to transition back to the root view controller from my second view controller, my app does not transition unless the button connected to the unwind method is pressed twice. The unwind method is called both times, however the transition only occurs upon the second call. I do not know why this occurs. Any help is appreciated. Thanks!
Unwind segue method
#IBAction func saveWordAndDefinition(segue:UIStoryboardSegue) {
self.searchController.active = false
if let definitionViewController = segue.sourceViewController as? DefinitionViewController {
saveWordToCoreData(definitionViewController.word)
}
tableView.reloadData()
}
How I linked my segue
Unwind segue
While what you're doing is permissible, it seems to be against best practice. The functionality of presenting a view controller, UITableViewController in this case, entering information, then later dismissing it with a button in the upper-right hand corner is generally associated with a modal view. In a push segue you'll get the back button in the upper-left corner for free, which will enable to you to pop the view controller off the stack without writing extra code.
Here's another Stack Overflow question that describe: What is the difference between Modal and Push segue in Storyboards?
To answer your question specifically, here are a couple links that should help:
[self.navigationController popViewControllerAnimated:YES]; is probably what you're looking for.
Dismiss pushed view from within Navigation Controller
How can I dismiss a pushViewController in iPhone/iPad?
So here's how I finally got this to work:
In my FirstViewController (the vc i'm unwinding to):
Here is my unwind segue method.
#IBAction func saveWordAndDefinition(segue:UIStoryboardSegue) {
self.navigationController?.popViewControllerAnimated(false)
}
Then I gave my unwind segue the identifier "unwind" in Storyboard.
In my SecondViewController (the vc i'm unwinding from):
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "unwind" {
if let destination = segue.destinationViewController as? VocabListViewController {
destination.saveWordToCoreData(word)
destination.tableView.reloadData()
}
}
}
I took care of passing data in the prepareForSegue method of my SecondViewController. Thanks to #Lory Huz for the suggestion. I finally figured out what you meant by it.
Works without any errors!
Related
I have a viewcontroller embedded in a navigationcontroller that pushes another viewcontroller onto the stack. This pushed viewcontroller has an embedded viewcontroller that segues/modally presents a final viewcontroller.
On a button click, I am trying to dismiss the final presented viewcontroller and pop the present-ing viewcontroller and return to the initial state.
Thus far, I've been able to get the dismiss going, but popping does not seem to work in the completion handler of the dismiss.
I've tried printing out the hierarchy, i.e. self.presentingViewController, self.navigationController, self.presentingViewController.presentingViewController..., all of which output nil, and am admittedly stuck now on returning to the initial state.
In looking at the view hierarchy, the final presented viewcontroller is beneath a UITransitionView separate from the rest of the stack I had mentioned earlier..
Any thoughts/guidance would be appreciated.
Since you mentioned segues I think unwind segues might help. I built a quick test project and they do indeed function correctly in your scenario.
There is a rather excellent answer in a related SO question What are Unwind segues for and how do you use them?. A summary of the answer for your particular case is: place the following function in your initial view controller:
#IBAction func unwindToThisViewController(segue: UIStoryboardSegue)
{
}
You can then directly 'unwind' to that viewcontroller by using Storyboard Segues directly (as in the referenced answer) or programatically via:
self.performSegue(withIdentifier: "unwindToThisViewController", sender: self)
Again there's a good article entitled Working with Unwind Segues Programmatically in Swift which goes into lots of detail.
Can you try
if let nav = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController {
self.dismiss(animated:true) {
nav.popToRootViewController(animated:true)
}
}
My issue is that I have two view controllers, and I need to transfer a string from one view controller to the next. I already have that down from a segue. The actual issue is that I need a label to change in the next view controller to make it match the currentTitle of the button in the previous view. And yes, there are multiple buttons.
My code from view 1 is this:
#IBAction func whichButtonWasClicked(sender: AnyObject) {
clickedButton = sender.currentTitle!!
}
// Segue - Pass the Data of which lesson was chosen
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let DestViewController : Lesson = segue.destinationViewController as! Lesson
DestViewController.lessonPlan = clickedButton
}
I connected all my 7 buttons to that same one action.
The problem is that when I transfer view controllers, and do...
print(lessonPlan)
It does not give me the title of the button from the action, because I guess that action is after the segue. And yes I tested it, and it IS after the segue. SO it does work, it's just a bit delayed, and I need it to be a part of the segue.
Since your IBAction is being called after prepareForSegue, you can use programmatic segues.
Right now, you're probably dragging a segue from your button to the next view controller. Instead, you can drag a segue from the current view controller to the next one (see this answer for a visual guide). This will create a programmatic segue that you can trigger whenever you'd like from code. Remember to give the segue an identifier in Xcode.
Then in whichButtonWasClicked, call self.performSegueWithIdentifier("identifier you setin Xcode", sender: sender) after you set clickedButton in order to trigger the segue. By doing so, you'll ensure that your button action is called before the segue starts.
I have IOS Swift program, using Storyboards with NavigationController.
There are two Views, lets call them mainView, secondView.
From the mainView I have BarButtonItem to go to secondView. When pressing that button, it triggers prepareForSegue function in the mainView with segue.identifier = "secondView"
When I have opend e.g. the secondView, I have two BarButtonItems for Cancel and Save. When pressing either of them the prepareForSegue function in that view is triggered, but now the segue.identifier = nil.
I would have expected to have the segue.identifier = "cancel" or "save" depended on the button pressed in that view.
Am I misunderstanding the segue functionality? Can anyone try to enlight me about this, as this looks like a very important and useful part of storyboards and navigation - but somehow I am not getting it right.
Have you created actions for the cancel and save buttons on your second view?
Right click and drag from your storyboard to the view controller code and select action from the dropdown.
Then in the action method, perform your segue.
#Garret, #rdelmar, #syed-tariq - thank you for pointing me into the right direction.
It turned out that Unwind Segue got me on track: Xcode Swift Go back to previous viewController (TableViewController)
But I also found one error I was doing in my storyboard, as I had Navigation Controller on all views (yes, I know - stupid when you know better): How do I segue values when my ViewController is embedded in an UINavigationController?
The final puzzle was to learn about the Protocols and Deligates to get this all to work.
Putting this together, then in short:
I created a protocol in my second view (above the class)
protocol MyDelegate: class {
func getMyList(sender: MySecondView) -> [NSManagedObject] //returns a CoreData list
}
Then I created a delegate variable in my second view
weak var datasource: MyDelegate!
In my first view I implemented the protocol, which was just one simple function returning a list that I needed in my second view
In my first view I have prepareForSegue where I catch the correct segue.identifier and there I set the delegate by going through segue.destinationViewController, like this
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
if segue.identifier == "mySegue" {
let vc = segue.destinationViewController as! MySecondView
vc.delegate = self
}
}
and that was about it - now magically the flow is correct, segues happening, deligates passing correctly, and all good :)
I've just figured out what is an unwind segue and how to use it with the great answers from this question. Thanks a lot.
However, here's a question like this:
Let's say there is a button in scene B which unwind segues to scene A, and before it segues I want something to be done, such as saving the data to database. I created an action for this button in B.swift, but it seems it goes directly to scene A without taking the action appointed.
Anyone knows why or how to do this?
Thank you.
You can do it the way you describe, or by using a prepareForSegue override function in the viewController you are unwinding from:
#IBAction func actionForUnwindButton(sender: AnyObject) {
println("actionForUnwindButton");
}
or...
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
println("prepareForSegue");
}
The first example is as you describe. The button is wired up to the unwind segue and to the button action in Interface Builder. The button action will be triggered before the segue action. Perhaps you didn't connect the action to the button in interface builder?
The second example gives you have access to the segue's sourceViewController and destinationViewController in case that is also useful (you also get these in the unwind segue's function in the destination view controller).
If you want to delay the unwind segue until the button's local action is complete, you can invoke the segue directly from the button action (instead of hooking it up in the storyboard) using self.performSegueWithIdentifier (or follow wrUS61's suggestion)
EDIT
you seem to have some doubts whether you can work this by wiring up your button both to an unwind segue and to a button action. I have set up a little test project like this:
class BlueViewController: UIViewController {
#IBAction func actionForUnwindButton(sender: AnyObject) {
println("actionForUnwindButton");
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
println("prepareForSegue");
}
}
class RedViewController: UIViewController {
#IBAction func unwindToRed(sender: UIStoryboardSegue) {
println("unwindToRed");
}
}
BlueViewController has a button that is connected in the storyboard to BOTH the unwindToRed unwind segue AND the actionForUnwindButton button. It also overrides prepareForSegue so we can log the order of play.
Output:
actionForUnwindButton
prepareForSegue
unwindToRed
Storyboard:
EDIT 2
your demo project shows this not working. The difference is that you are using a barButtonItem to trigger the action, whereas I am using a regular button. A barButtonItem fails, whereas a regular button succeeds. I suspect that this is due to differences in the order of message passing (what follows is conjecture, but fits with the observed behaviour):
(A) UIButton in View Controller
ViewController's button receives touchupInside
- (1) sends action to it's method
- (2) sends segue unwind action to storyboard segue
all messages received, and methods executed in this order:
actionForUnwindButton
prepareForSegue
unwindToRed
(B) UIBarButtonItem in Navigation Controller Toolbar
Tool bar buttonItem receives touchupInside
- (1) sends segue unwind action to storyboard segue
- (2) (possibly, then) sends action to viewController's method
Order of execution is
prepareForSegue
unwindToRed
actionForUnwindButton
prepareForSegue and unwind messages received. However actionForUnwindButton message is sent to nil as viewController is destroyed during the segue. So it doesn't get executed, and the log prints
prepareForSegue
unwindToRed
In the case of (B), the viewController is destroyed before the method reaches it, so does not get triggered
So it seems your options are...
(a) use a UIButton with action and unwind segue
(b) trigger your actions using prepareForSegue, which will be triggered while the viewController is still alive, and before the segue takes place.
(c) don't use an unwind segue, just use a button action. In the action method you can 'unwind' by calling popToViewController on your navigation controller.
By the way, if you implement a toolBar on the viewController (not using the navigation controller's toolbar) the result is the same: segue gets triggered first, so button action fails.
If you are able to perform unWind Segue Successfully. Then the method in destination View Controller is called just before the segue take place, you can do what ever you want in source viewcontroller by using the segue object.
- (IBAction)unwindToThisViewController:(UIStoryboardSegue *)unwindSegue
{
CustomViewController *vc = (CustomViewController*) unwindSegue.sourceViewController;
[vc performAnyMethod];
[vc saveData];
NSString *temp = vc.anyProperty;
}
if you want your logic in source Controller then implement prepareForSegue in Scene B and set the unWind segue Identifier from Storyboard > Left View hierarchy Panel > under Exit in Scene B.
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:#"backToSource"])
{
NSLog(#"Going Back");
}
}
At first, you should call the send data function in prepareForSegue method.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([[segue identifier] isEqualToString:#"UnwindIdentifier"]) {
// send data
}
}
If you don't want to let unwind segue happen before getting response from the server, you should override
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender
method and return NO;. Then you can perform segue manually when you get the server response by calling:
[self performSegueWithIdentifier:#"UnwindIdentifier" sender:sender];
I am trying to transition (modal) via segue to a UINavigationController from a UIViewController.
My UIViewController andUINavigationController are in interface builder.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
self.performSegueWithIdentifier("showNavController", sender: self)
}
However it keeps giving me a warning:
Warning: Attempt to present <UINavigationController: 0x14d5095e0> on <Test.ViewController: 0x14d60d9d0> whose view is not in the window hierarchy!
Why are you calling performSegueWithIdentifier inside prepareForSegue?
That is what is causing the error. You don't have to call the performSegueWithIdentifier again.
performSegueWithIdentifier is used to perform a segue operation. prepareForSegue is called after you call performSegueWithIdentifier function and before the segue transition happens.
You need to define a root view relationship for your Navigation Controller, otherwise it has nothing to display.
A navigation controller is a container view controller—that is, it
embeds the content of other view controllers inside of itself.
Every navigation stack must have at least one view controller to act
as the root.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/index.html