Pop Up ViewController from Navigation Stack - ios

I'm studying the tutorial from Apple Developer: Start Developing iOS Apps (Swift), and I'm confused with push and modal segue.
There are two scenarios, Save and Cancel button in navigation bar, backing to scene 1 from scene 2.
If the Cancel button is pressed, it will call different method for dismissing scene 2:
#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.
let isPresentingInAddMealMode = presentingViewController is UINavigationController
if isPresentingInAddMealMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
else {
fatalError("The MealViewController is not inside a navigation controller.")
}
}
In this method, if the scene is presented by modal segue, dismiss(animated:completion:) is called, and
if the scene is presented by push segue, popViewController(animated:) is called for poping up the ViewController from the navigation stack.
But for the Save button, the tutorial overrides a method in scene 2, prepare(for:sender:), and a action method in scene 1, unwindToMealList(sender:).
And it drag the Save button to Exit (the button in the scene dock) and choose unWindToMealList(sender:) method.
So the flow will be: prepare(for:sender:) -> scene 2 dismissed and scene 1 presented -> unWindToMealList(sender:)
I'm wondering that the code snippets didn't dismiss explictly scene 2 and remove the ViewController in navigation stack when Save button is pressed.
I know that modal segue won't push ViewController to navigation stack, but push segue will push it.
Why the code snippets doesn't pop up it from navigation stack?
Many thanks.

It seems like the tutorial you are reading is making use of unwind segues.
Unwind segues, just like normal segues, have a source and a destination and you can prepare for it in prepareForSegue, but instead of presenting the destination VC, it will dismiss the source VC so that the destination VC is shown.
Unwind segues behave differently in different situations. When you present VC B from VC A using a push segue, and an unwind segue from B to A, the unwind segue will pop VC B from the navigation stack. When you present VC B from VC A modally, the unwind segue will dismiss the modally presented VC.
As you can see, unwind segues are quite smart. It will decide for itself what to do in order to show the destination VC. It can even pop two or more VCs in the navigation stack!

Related

TabBar is hidden after going back to the initial ViewController of a TabBarController Using Segue

I have a tab bar controller with three table view controllers and the second VC is embedded in a navigation Controller. in the second VC, I made the tabBar hidden using this line self.tabBarController?.tabBar.isHidden = true and I created a bar button to go back to the first view controller which is the "home" VC using segue with modal presentation.
Screenshot of my StoryBoard
My problem is after hitting the back button and going back to home VC from the second VC, the tabBar is still hidden even though I put self.tabBarController?.tabBar.isHidden = false in the home VC's viewWillAppear method and the second VC's viewWillDisappear method.
Here is the result that I expected vs what I got
expected home VC
result home VC
How can I make the Tab Bar show?
When you are using the modal presentation segue, you are creating a completely new instance of HomeViewController. The new HomeViewController is not linked to the TabBarController in your hierarchy.
Here's you initial view hierarchy:
TabBarController
-> HomeVC
-> CreateVC (Navigation Controller)
-> CreateQuizVC
-> SavedVC
Now after tapping the back button you'll get the following:
TabBarController
-> HomeVC
-> CreateVC (Navigation Controller)
-> CreateQuizVC
-> HomeVC(2)
-> SavedVC
What you could do is, instead of using the segue to go back, add an IBAction in your code to set the selectedIndex of the TabBar programatically, and link the Back UIBarButtonItem to this IBAction.
#IBAction func backButtonAction(_ backButton: UIBarButtonItem) {
// Keep in mind that the CreateQuizVC is embeded in a NavigationController.
// The NavigationController is the child of the TabBarController
navigationController?.tabBarController?.selectedIndex = 0
navigationController?.tabBarController?.tabBar.isHidden = false
}
However, my suggestion is you use the TabBar as it's intended by Apple. Don't hide it while you're presenting your CreateQuizVC, and use the TabBar to navigate between the tabs. This will help with user experience, since everybody on iOS is expecting this behaviour from a TabBar.

Dismissing Modally Presented VC and Popping Presenting VC

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)
}
}

Dismiss or remove previous modally presented view controller as soon as the next one appear modally

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.

Present View Controller Over current tabBarController with NavigationController

When presenting or dismissing VC, I do not want to keep hiding and showing tabBar because it creates a poor user experience. Instead, I want present the next VC straight over the tab bar such that when I dismiss the nextVC by dragging slowly from left to right, I can see the tabBar hidden behind the view (As shown in image below)
Note, my app has two tabs with two VCs(VCA,VCB) associated to it. Both VC also have navigation bar embedded. VCA segues to VCA1 and VCB segues to VCB1. At the moment, inside VCA and VCB I am calling the following function to segue with some hiding and unhiding done when viewWillappear (Code below).
self.navigationController?.showViewController(vc, sender: self)
// Inside ViewWillAppear Only reappear the tab bar if we successfully enter Discover VC (To prevent drag back half way causing tab bar to cause comment entry to be floating). This code check if we have successfully enters DiscoverVC
if let tc = transitionCoordinator() {
if tc.initiallyInteractive() == true {
tc.notifyWhenInteractionEndsUsingBlock({(context: UIViewControllerTransitionCoordinatorContext) -> Void in
if context.isCancelled() {
// do nothing!
}
else {
// not cancelled, do it
self.tabbarController.tabBar.hidden = false
}
})
} else {
// not interactive, do it
self.tabbarController.tabBar.hidden = false
}
} else {
// not interactive, do it
self.tabbarController.tabBar.hidden = false
}
----------Working solution from GOKUL-----------
Gokul's answer is close to spot on. I have played with his solution and came up with the following improvement to eliminate the need to have a redundant VC and also eliminate the initial VC being shown for a brief second before tabVC appears. But without Gokul, I would never ever come up with this!!
Additionally, Gokul's method would create a bug for me because even though I do have a initial "normal" VC as LoginVC before tabVC is shown. This loginVC is ONLY the rootVC if the user needs to login. So by setting the rootVC to tabVC in most cases, the navVC will never be registered.
The solution is to embed navigation controller and tabBar controller to one VC. But it ONLY works if the navVC is before the TabBarVC. I am not sure why but the only way that allowed me to have navVC-> tabVC-> VC1/VC2 is to embed VC1 with a navVC first than click on VC1 again to embed tabVC (It wouldn't allow me to insert one before tabVC and I also had to click the VC1 again after embedding the NavVC).
For your requirement we need to make some small changes in your given view hierarchy
Let me explain step by step,
To meet your requirement we have to add a UIViewController(let's say InitialVC) embedded with a UINavigationController and make it as initial viewcontroller.
Then add a UITabbarController with 2 VC (VCA,VCB) // IMPORTANT: Without any navigationcontroller embedded.
Add a segue between InitalVC and TabbarController with an unique identifier(ex: Initial)
In viewWillAppear of InitalVC perform segue as below (InitialVC is unnecessary to our design we are using this just to bridge navigationController and tabbarController).
self.performSegueWithIdentifier("Initial", sender: nil)
In TabbarControllerclass hide your back button, this ensures that InitialVC is unreachable.
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true
}
Now add a segue from a button between VCA and VCA1, thats it build and run you will see VCA1 presenting over VCA's tabbar.
What we have changed?
Instead of adding UINavigationController inside UITabbarController we have done vice versa. We can't directly add Tabbar inside navigation to do that we are using InitialVC between them.
Result:
1st way is create a image of the tabbar using UIGraphicsGetImageFromCurrentImageContext and set it on the bottom of the other view...
2nd way is show the next view in another new window that is above the tabbar, that way you wont need to hide the tabbar anymore, but seems like its in the navigation controller so this way doesnt seems available
Hiding and unhiding the tab bar is unnecessary. You only need to embed the UITabBarController inside the UINavigationController. That is, UINavigationController as the initial vc, UITabBarController as the root vc of UINavigationController.

Programmatically defining a new navigation controller order/stack?

I have a NavigationController that the following VC's are embedded in: VC1 -> VC2 -> VC3 -> VC4 -> VC5. My problem is that when I segue from VC5 (after editing is completed), I send you back to VC3, but I want to programmatically throw VC4 and VC5 off the stack, i.e. when the user is sent back to VC3, I want "back" in the navagitionBar to take you to VC2 (and not VC5 where you really came from).
This comes up a lot in IOS, where you want to edit the model, then send them back to the tableView/Collection view, but since editing is done, you don't want the editing viewControllers in the navigation stack anymore as its too confusing of UX.
In the screenshot below, the VC on the top right is VC5: which is segued back to the PinViewController (VC3) via self.performSegueWithIdentifier("backToPins", sender: self)
How can I do this?
don't use segue to come back (pop).
you should use popToViewController and pass specific viewcontroller as argument to pop that viewcontroller.
for example if you want to go on 3rd view controller out of five then you can do something like below. you can just change index from viewcontroller array to go different view controller.
let viewControllers: [UIViewController] = self.navigationController!.viewControllers as [UIViewController];
self.navigationController!.popToViewController(viewControllers[viewControllers.count - 3], animated: true);
If you are using segue that means you add (push) new viewcontroller to navigation stack. in your example your stack after reaching 5th view is like,
VC1 - VC2 - VC3 - VC4 - VC5 (top of stack)
now if you performsegue to go back to VC3 then stack should be like this,
VC1 - VC2 - VC3 - VC4 - VC5 - VC3(top of stack)
and if you pop to VC3 then your stack is like,
VC1 - VC2 - VC3 (top of stack).
so pop viewcintrollers to go back don't use segue
hope this will help :)
The best way to do this is via an unwind segue.
In VC3 you define an appropriate unwind function:
#IBAction func unwind(segue:UIStoryboardSegue) {
if let sourceViewController = segue.sourceViewController as? VC5 {
let myNewData=sourceViewController.someProperty
self.someFunctionThatUpdatesScene()
}
Then in the VC5 scene you can create an unwind segue in one of two ways.
If you want it to be triggered directly from an object, such as a UIButton, you drag from the action in the inspector to the exit icon at the top of the scene and select unwind from the pop up.
If you want to trigger the unwind programatically then you drag from the view controller object in the explorer on the left to the exit icon and select unwind from the popup. You will now see an unwind segue in the explorer and you can give it an identifier like you would with any segue. You can use this identifier with performSegueWithIdentifier
The advantage of this approach is that you don't need to make any assumptions about the depth of UINavigationController stack and you don't need to implement a delegate/protocol to pass data back.
Apple has a very good Tech Note on Using Unwind Segues

Resources