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)
}
}
Related
I'm coding a simple app with swift and I'm stuck at the following point, I have two Controllers that lead to another one, and when I click on the cancel button, it always lead to the root Controller, no matter from where I come.
I have a first controller (UIViewController), that go to the Navigation Controller of my target Controller (the one from which I would like to go back to the right calling Controller).
I have a second controller (UITableViewController), which go directly to my target Controller.
Here's the code of my Cancel button:
// MARK: - Navigation
#IBAction func lendingCancelButton(_ sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways
let isPresentingInAddLendingMode = presentingViewController is UINavigationController
if isPresentingInAddLendingMode {
dismiss(animated: true, completion: nil)
} else if let owningNavigationController = navigationController {
owningNavigationController.popViewController(animated: true)
} else {
fatalError("the LendingViewController is not inside a navigation controller.")
}
}
If I correctly understood (you could then correct me if I'm wrong, I would learn something), it's testing if the ViewController that's presenting my target ViewController is a NavigationController.
So maybe that, as the second Controller (my UITableViewController) is not going through a NavigationController, so the last one calling my target view with a NavigationController is always the UIViewController.
Don't hesitate to tell me if it's not clear enough (too many times the word "Controller" in my post) or if you need additional code.
Try something like this
if let navigationController = presentingViewController as UINavigationController {
navigationController.popViewController(animated: true)
} else if let viewController = presentingViewController as UIViewController {
dismiss(animated: true, completion: nil)
} else {
fatalError("the LendingViewController is not inside a navigation controller.")
}
If i understood you want to use dismiss when you find a UIViewController and to pop the navigation when you find a UINavigationController right?
Ok so I finally found a way to make it working.
My tableViewController was embedded into a NavigationController. I removed it (since I could do without it, according to my need). From this View Controller, I draw a segue that "Show" my target view.
From my other ViewController (this one is embedded into a NavigationController), I draw a segue put that present modally my target view.
With the code provided in my initial post, it's working.
The only thing I didn't understand is why the NavigationController from my TableViewController was likely to cause it not working properly.
Following Apple's documentation for adding and editing information Apple guide here I have a Viewcontroller with a tableview. The tableview contains a header with a "Add new" Button. If a table row is selected the detailViewController is pushed onto the stack. The detailViewController is also embedded in a UINavigationController, as in Apple's docs. If "Add new" is pressed, another segue is performed which presents the UINavigationController modally, which in turns shows the detailViewController. This works fine and the animation clearly shows a modally presented ViewController.
The detailViewController contains a Cancel Button in the NavigationBar. If it is pressed the following code is run:
#IBAction func cancel(_ sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
var isPresentingInAddActionMode = false
if let presentingVC = self.presentingViewController{
isPresentingInAddActionMode = presentingVC is UINavigationController
}
streekgidsModel.undoManager.endUndoGrouping()
print("undo grouping ended and undone")
streekgidsModel.undoManager.undo()
if isPresentingInAddActionMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
else {
fatalError("The MealViewController is not inside a navigation controller.")
}
}
The first if-statement checks if the property presentingViewController is present, and if so if it is of type UINavigationController. If so, the viewController is presented modally and should be dismissed. If not it is pushed onto the stack and the owningNavigationController should pop the detailViewController.
Running this code does not work as described by Apple. The check on the presentingViewController shows it is present, but the type check gives back "invalid". This is treated as false. The test on the owningNavigationController succeeds (I think it should fail) and the popViewController is executed. As there was no push, the view controller is not popped or dismissed and is still visible. A second press on Cancel executes the func cancel again, which results in an error as there is no longer a group started in the undo manager.
Baffling thing is that I have the same code in another viewcontroller, with similar UIViewTable and navigation and it works fine.
So to frame the question: why does this not work the way Apple describes it, why does my other view controller work as it is supposed to? Any input is appreciated.
BTW, the fatal error text is straight from the docs so the naming is not relevant and it is never executed.
I would start with checking who is presenter.
According to Apple docs on this:
When you present a view controller modally (either explicitly or implicitly) using the present(_:animated:completion:) method, the view controller that was presented has this property set to the view controller that presented it. If the view controller was not presented modally, but one of its ancestors was, this property contains the view controller that presented the ancestor. If neither the current view controller or any of its ancestors were presented modally, the value in this property is nil.
If the docs are correct then your presenter should be your "Viewcontroller with a tableview" which, I guess, is not UINavigationController. If that is the case then you should understand why your code fails.
It depends on your context of course, but I would just simplify a check this way:
var isPresentingInAddActionMode = self.presentingViewController != nil
... // your other code
if isPresentingInAddActionMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
If I understood your question and intent correctly then it doesn't matter for you who (which class) presented your detailVC and you care only about how your detailVC was presented - either pushed in navigation view controller or presented modally. I think just by checking presentingViewController property you can get that information.
My target include a lot view need to present different view modally base on each user action. Here what I want to do to get cleaner view hierarchy and better user experience.
Root View Controller present First View Controller modally
When I clicked button on the First View Controller, then the Second View Controller appear modally over it.
As soon as the Second View Controller did appear, I want to dismiss or remove the first one from view hierarchy.
Can I do that? If so, how should i do it?
If not, what is the right way to solve this out cause I will present many modally presented view controllers over each view. I think even if I want to dismiss current view, the previous one will still remain appear when current one dismiss.
UPDATE :
VC1 (Root) > VC 2 (which was present modally) > VC 3 (which was
present modally over VC 2)
When i dismiss VC3, the VC2 is still on view memory. So, I don't want to appear VC2 as soon as I dismiss VC3 and instead I want to see VC1 by removing or dismissing VC2 from view hierarchy.
WANT : At the image, when I dismiss the blue,I don't want see the pink in my view memory and I want to remove it as soon as the blue one appear.
That's what i want to do.
Any Help?Thanks.
So, let's assume that you have a storyboard similar to:
What should happens is:
Presenting the the second ViewController (from the first ViewController).
Presenting the the third ViewController (from the second ViewController).
dismissing to the first ViewController (from the third ViewController).
In the third ViewController button's action:
#IBAction func tapped(_ sender: Any) {
presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
}
As you can see, by accessing the presentingViewController of the current ViewController, you can dismiss the previous hierarchy of the view controllers:
The view controller that presented this view controller.
By implementing presentingViewController?.presentingViewController? that means that: the presented of the presented current ViewController :)
It might seem a little bit confusing, but it is pretty simple.
So the output should be like (I added background colors to the viewControllers -as vc1: orange, vc2: black and vc3: light orange- to make it appears clearly):
EDIT:
If you are asking to remove the ViewController(s) in the middle (which in this example the second ViewController), dismiss(animated:completion:) does this automatically:
If you present several view controllers in succession, thus building a
stack of presented view controllers, calling this method on a view
controller lower in the stack dismisses its immediate child view
controller and all view controllers above that child on the stack.
When this happens, only the top-most view is dismissed in an animated
fashion; any intermediate view controllers are simply removed from the
stack. The top-most view is dismissed using its modal transition
style, which may differ from the styles used by other view controllers
lower in the stack.
Referring to what are you asking:
I think even if I want to dismiss current view, the previous one will
still remain appear when current one dismiss.
I think that appears clearly on the UI (and I find it ok), but as mentioned in the dismiss documentation discussion, both the third and the second will be removed from the stack. That's the right way.
Here is my opinion in different perspective,
Root View Controller present Second View Controller
Add FirstView onto Second View
Dismiss FirstView Controller when button pressed.
Second View Controller,
class ViewController: UIViewController, FirstViewControllerProtocol {
weak var firstViewController: FirstViewController?
override func viewDidLoad() {
super.viewDidLoad()
print("Not initiated: \(firstViewController)")
firstViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "FirstViewController") as? FirstViewController
addChildViewController(firstVC!)
firstViewController?.delegate = self
view.addSubview((firstViewController?.view)!)
print("Initiated: \(firstViewController)")
}
func dismiss() {
firstViewController?.view.removeFromSuperview()
firstViewController?.removeFromParentViewController()
}
}
FirstViewController,
protocol FirstViewControllerProtocol {
// Use protocol/delegate to communicate within two view controllers
func dismiss()
}
class FirstViewController: UIViewController {
var delegate: FirstViewControllerProtocol?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func dismiss(_ sender: Any) {
delegate?.dismiss()
}
deinit {
print("BYE")
}
}
What you want is an "unwind segue":
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/UsingSegues.html#//apple_ref/doc/uid/TP40007457-CH15-SW8
https://developer.apple.com/library/archive/technotes/tn2298/_index.html
It allows you to dismiss multiple view controllers at the same time, without having to know how many there are in the stack.
In VC1 you would implement an IBAction called (for instance) unwindToRoot. Then in the storyboard for VC3, you wire up your Done button to the Exit object and choose the unwindToRoot action.
When that button is pressed, the system will dismiss all the view controllers it needs to bring you back to VC1.
This is better than calling presentingViewController?.presentingViewController?.dismiss(), because VC3 doesn't need to know anything about the view controller hierarchy underneath it.
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!
I'm presenting a modal view controller using a custom transition (by setting its modelPresentationStyle to UIModalPresentationCustom, providing a transitioning delegate, and UIViewControllerAnimatedTransitioning object).
In the presented view controller, I have an unwind segue hooked up to a button. The segue fires just fine; the IBAction method in the presenting view controller is called, and so is prepareForSegue in the presented view controller. However, the presented view controller is not dismissed, and the appropriate transitioning delegate method (animationControllerForDismissedController:) is not called.
If, however, I set the presented view controller's modalPresentationStyle to UIModalPresentationFullScreen (the default), the view controller is dismissed properly (this breaks my custom transition, though).
I'm at a complete loss at what to do here. I've looked through Apple's documentation, and didn't notice anything saying that one had to do special things with unwind segues when dealing with custom transitions.
I'm aware that I could call dismissViewControllerAnimated:completion: in the IBAction method of the presenting view controller, but I'd rather use that as a last resort, and get the unwind segue working the way it should (or at least know why it's not working :) ).
Any help would be much appreciated,
Thanks in advance
It seems that if you use UIModalPresentationCustom to present the controller with a custom transition manager, you also need to use a custom transition manager to dismiss it (which makes sense I guess, since you can do all kinds of weird stuff in the animator object and UIKit can't be sure that just dismissing the screen as usual will completely restore the original state - I just wish it told you that explicitly...).
Here's what I've done to fix this in my app:
override segueForUnwindingToViewController in the parent view controller (the one to which you're moving after the dismiss animation) and return an instance of your UIStoryboardSegue, either the one you've used for the original transition or a new separate class
if the unwind segue's target view controller is in a navigation hierarchy, then you need to override that method in the navigation controller instead
in the perform method call dismissViewControllerAnimated
the presented view controller needs to still hold a valid reference to the transitioning delegate, or you'll get an EXC_BAD_ACCESS (see DismissViewControllerAnimated EXC_Bad_ACCESS on true) - so either make it keep the delegate as a strong reference as described in that thread, or assign a new one before calling dismissViewControllerAnimated (it's possible that changing modelPresentationStyle to e.g. full screen before dismissing would work too, but I haven't tried that)
if the dismiss animation needs to do any non-standard things (mine luckily didn't), override animationControllerForDismissedController in the transition manager object and return a proper animator
if the target view controller is in a navigation hierarchy, then you also need to manually pop the navigation stack to the target controller before dismissing the presented screen (i.e. target.navigationController!.popToViewController(target, animated: false))
Complete code sample:
// custom navigation controller
override func segueForUnwindingToViewController(toViewController: UIViewController,
fromViewController: UIViewController,
identifier: String?) -> UIStoryboardSegue {
return CustomSegue(
identifier: identifier,
source: fromViewController,
destination: toViewController
)
}
// presented VC
var customTransitionManager: UIViewControllerTransitioningDelegate?
// custom segue
override func perform() {
let source = sourceViewController as! UIViewController
if let target = destinationViewController as? PresentedViewController {
let transitionManager = TransitionManager()
target.modalPresentationStyle = .Custom
target.customTransitionManager = transitionManager
target.transitioningDelegate = transitionManager
source.presentViewController(target, animated: true, completion: nil)
} else if let target = destinationViewController as? WelcomeViewController {
target.navigationController!.popToViewController(target, animated: false)
target.dismissViewControllerAnimated(true, completion: nil)
} else {
NSLog("Error: segue executed with unexpected view controllers")
}
}
I also met this problem when I need to pass data back from the modalpresented view.
I wandering around Google and here for a couple of hours but I couldn't find an answer that is easy to understand for me. But I did get some hint and here's a work around.
It seems that because it has to pass data back, and the dismissing process from the automatic Unwind is prior before the data passing which prevented the ViewController being dismissed. So I think that I have to manually dismiss it once one more time.
I got some luck here. I didn't notice that it was a child viewcontroller. I just configured it from the storyboard.
And then in the Unwind function, I added to lines to remove the child viewcontroller and the child view. I have no code in the sourceViewController.
Swift 4.1
#IBAction func unwindToVC(sender :UIStoryboardSegue){
if let source = sender.source as? CoreLocationVC{
if source.pinnedCity != nil{
clCity = source.pinnedCity
}
if source.pinnedCountry != nil {
clCountry = source.pinnedCountry
}
if source.pinnedTimeZone != nil {
clTimeZone = source.pinnedTimeZone
}
if source.pinnedLocation != nil {
clLocation = source.pinnedLocation
}
// I added 2 lines here and it just worked
source.view.removeFromSuperview()
source.removeFromParentViewController()
}