I have inherited a navigation controller issue in an existing app that I'm trying to solve cleanly.
This app has multiple storyboards and multiple UINavigationControllers. At one point in the app, a series of view controllers is presented modally, using a separate storyboard and a separate nav controller. When the modal process is complete, the navigation hierarchy looks something like this:
NavController1 -> VC1 ['Present Modally' segue] -> NavController2 -> VC2 -> VC3 -> VC4
When the user completes the modal activity in VC4, dismiss() is called programmatically on VC4 and the user can then navigate back to VC1 using the back button.
However, what we really need to do is to 'pop off' all of the modally presented set of view controllers (and their nav controller) when the user finishes the modal activity. The problem is that from VC3 or VC4 I can't call popToRootViewController(). I also can't traverse down the VC stack to find VC1, since the current Nav controller doesn't manage it.
A couple solutions come to mind:
1) use the notification manager and have VC1 listen for the message to pop everything off back to itself
2) pass a reference to VC1 as a delegate all the way up the chain so that VC3 or 4 can have it pop everything off
Both of these solutions follow the general maxim that the presenting VC should be the one that dismisses, but neither are what I would consider clean.
I would welcome any thoughts or alternative solutions.
Assuming that these are the way the view controllers were laid out:
NavController1 --['Root View Controller' segue]--> VC1 --['Present Modally' segue]--
--> NavController2 --['Root View Controller' segue]--> VC2 --['Push' segue]--> VC3 --['Push' segue]--> VC4
You should be able to go back to VC1 by dismissing either VC2, VC3 or VC4.
// example for vc4
vc4.navigationController?.dismiss(animated: true, completion: nil)
However, if each of the viewControllers were presented modally, you should be able to traverse through the presentingViewController to reach VC1.
var currentVC: UIViewController? = self
var presentingVC: UIViewController? = currentVC?.presentingViewController
while presentingVC != nil && !(presentingVC is VC1) {
currentVC?.dismiss(animated: true, completion: nil)
currentVC = presentingVC
presentingVC = currentVC?.presentingViewController
}
Hope that helps.
When popping, you may kick out the viewControllers from your navigation Controller, would solve your problem
extension UINavigationController {
public func removeViewController(classes : [String]) {
var vcs = [UIViewControllers]()
for viewController in self.viewControllers {
let name = viewController.className
if !classes.contains(name) {
vcs.append(viewController)
}
}
if classes.count < vcs.count {
self.viewControllers = vcs
}
}
}
now think you have 4 viewControllers , A, B, C, D, you want to remove B and C and Move Back To A
In D's View Controller
override func viewDidLoad() {
super.viewDidLoad()
//your works
let viewControllersToRemove = [String(describing: type(of:B)), String(describing: type(of:C))]
navigationController.removeViewControoler(classes : viewControllersToRemove)
}
Related
I have 5 VC's, I'm successfully removing ViewController from navigation stack. But the problem is when click back button on navigation, it's moving into previous VC and it's showing removed VC on navigation bar.
Ex: I have 5 VC's: VC1, VC2, VC3, VC4, VC5.
Now I'm navigating from VC1 -> VC2, ..... VC4 -> VC5. And I have custom navigation bar back button title. Here I'm removing VC4 from stack.
When click back button in VC5 it's directly moving into VC3. But navigation bar is VC4. When click navigation bar once again now it's displaying VC3 navigation bar in same VC.
HOW TO resolve this issue. I want to display directly VC3 and vc3 navigation bar in single click.
Code to remove VC from Navigation stack:
guard let navigationController = self.navigationController else { return }
var navigationArray = navigationController.viewControllers // To get all UIViewController stack as Array
navigationArray.remove(at: navigationArray.count - 2) // To remove previous UIViewController
self.navigationController?.viewControllers = navigationArray
Use the following:
navigationController?.setViewControllers(navigationArray!, animated: true)
E.g.
guard let navigationController = self.navigationController else { return }
var navigationArray = navigationController.viewControllers
navigationArray.remove(at: navigationArray.count - 2)
navigationController.setViewControllers(navigationArray!, animated: true)
From the docs:
Use this method to update or replace the current view controller stack
without pushing or popping each controller explicitly. In addition,
this method lets you update the set of controllers without animating
the changes, which might be appropriate at launch time when you want
to return the navigation controller to a previous state.
If animations are enabled, this method decides which type of
transition to perform based on whether the last item in the items
array is already in the navigation stack. If the view controller is
currently in the stack, but is not the topmost item, this method uses
a pop transition; if it is the topmost item, no transition is
performed. If the view controller is not on the stack, this method
uses a push transition. Only one transition is performed, but when
that transition finishes, the entire contents of the stack are
replaced with the new view controllers. For example, if controllers A,
B, and C are on the stack and you set controllers D, A, and B, this
method uses a pop transition and the resulting stack contains the
controllers D, A, and B.
Edit 1
When you are pushing VC5, use the following code
let vc = YourVC5()
var array = navigationController?.viewControllers
array?.removeLast()
array?.append(vc)
navigationController?.setViewControllers(array!, animated: true)
The idea is when you push VC5 into stack, before pushing we are excluding VC4 from the list thus it will have VC3 beneath VC5 by default and you just need to call the navigationController?.popViewController(animated: true) and it should pop directly to VC3
Hide default back button and add custom back button with action:
override func viewDidLoad {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true
let customBackButton = UIBarButtonItem(title: "Back", style: UIBarButtonItem.Style.plain, target: self, action: #selector(back))
self.navigationItem.leftBarButtonItem = customBackButton
}
Use popToViewController to move back to specific viewcontroller:
#objc func back(sender: UIBarButtonItem) {
guard let navigationController = self.navigationController else { return }
var navigationArray = navigationController.viewControllers // To get all
self.navigationController!.popToViewController(navigationArray[navigationArray.count - 2], animated: true)
}
if you are using custom NavigationBar than you need to use custom back button click Action in VC5 :-
#IBAction func btnBackAction(_ sender: UIButton) {
let vc = VC3()
self.navigationController.popToViewController(vc, animated: true)
}
And if you can use Default NavigationBar than need to remove VC4 in navigation stack in VC5 like this:-
guard let navigationController = self.navigationController else { return }
var navigationArray = navigationController.viewControllers // To get all UIViewController stack as Array
navigationArray.remove(at: navigationArray.count - 2) // To remove previous UIViewController
self.navigationController?.viewControllers = navigationArray
You can use popToViewController(_:animated:) (as Prakash Shaiva answered above):
guard let navigationController = self.navigationController else { return }
var navigationArray = navigationController.viewControllers // To get all
self.navigationController.popToViewController(navigationArray[navigationArray.count - 2], animated: true)
And try to update your NavigationBar in the method viewWillAppear(_:) for VC3.
I have in my project tow view controller and I linked the first VC with navigation controller but the problem is : I used present to go to second VC (that mean I didn't use segue) ... how can I set back to first VC in navigation Controller by code (without segue) .
picture from my storyboard
my code :
let storyboard = self.storyboard
let viewcontroller = storyboard?.instantiateViewController(withIdentifier: "contact_detail") as! ViewController2
viewcontroller.arr2 = arr
present(viewcontroller, animated: true, completion: nil)
With the dismiss method it will work.
Dismisses the view controller that was presented modally by the view controller. (Apple Docs)
self.dismiss(animated: true, completion: nil)
If you are presenting a viewcontroller with present method, you can dismiss it with dismiss method.
If you are adding any view controller with push method then only it will get added to your navigation stack and you can remove it by calling popviewcontroller on it's back action.
Here, you are presenting a viewcontroller, hence it will not get added to your first navigation stack and you can not remove it with pop action on click of back.
If you are looking for a back button like feature on presented view controller, you can add a back button in toolbar, dismiss a viewcontroller on back button action, and can animate it like a popnavigation while dismissing.
extension UINavigationController {
public func removeViewController(classes : [String]) {
var vcs = [UIViewControllers]()
for viewController in self.viewControllers {
let name = viewController.className
if !classes.contains(name) {
vcs.append(viewController)
}
}
if classes.count < vcs.count {
self.viewControllers = vcs
}
}
}
now think you have 4 viewControllers , A, B, C, D, you want to remove B and C and Move Back To A
In D's View Controller
override func viewDidLoad() {
super.viewDidLoad()
//your works
let viewControllersToRemove = [String(describing: type(of:B)), String(describing: type(of:C))]
navigationController.removeViewControoler(classes : viewControllersToRemove)
}
I solved this by using pushViewController
self.navigationController?.pushViewController(MyViewController, animated: true)
Scenario: I have a view controller (vc1) that presents another view controller (vc2). And then vc2 presents vc3. How can I set vc1 as a delegate for vc3?
Situation: While in vc3, I intend to pop back to vc1 and have it execute some code. But, since the vc3 instance is created in vc2, vc1 has no direct link to vc3 from vc1.
Here is a simplified picture of what I'm trying to achieve:
vc1
let vc2Instance = vc2()
navigationController?.pushViewController(vc2Instance)
class vc1: UIViewController, tellVC1ToDoSomethingDelegate {
func vc1DoSomething(withThis: String) {
// for instance: present another VC not currently in stack
let randomVC = RandomVC()
}
}
vc2
let vc3Instance = vc3()
navigationController?.pushViewController(vc3Instance)
vc3
protocol tellVc1ToDoSomethingDelegate() {
func vc1DoSomething(withThis: String)
}
class vc3: UIViewController {
weak var vc1Delegate: tellVC1ToDoSomethingDelegate?
func pushRandomVCWithString(myString: String) {
// code to dismiss view controllers up to vc1 in stack
if let vcStack = self.navigationController?.viewControllers{
self.navigationController?.popToViewController(vcStack[vcStack.count - 2], animated: true)
vc1Delegate.vc1DoSomething(withThis: myString)
}
}
Here's my issue:
If I was marking vc1 as a delegate for vc2 (Just 1 VC up the stack), I'll simply type
let vc2Instance = vc2()
vc2Instance.vc1Delegate = self
How do I access self (of vc1) when no instance of vc3 exists in vc1? Is chaining delegates from vc3 to vc2 then to vc1 the only way out? I imagine this will be ugly when there are several vc's in-between.
There are two way to do this, one is delegation and another one is Notification.
Delegation:
As you asked, you are trying this one. Just:
create a delegate variable in vc2 and vc3 both.
while creating the object of vc2 in vc1 pass the self in the delegate variable of vc2.
while creating the object of vc3 in vc2 pass the self.delegate in the delegate variable of vc3.
Now as you call the method in vc3 like self.delegate?.anyMethod() will get the call in both vc2 and vc3.
Notification:
As of the notification you can broadcast a notification of custom type with some info for vc3. and can add observer to observe the notification in either in vc1 or any other vc, you will receive the call with passed data.
Learn more about Notification here
As my personal suggestion first one is better in your case.
In many ways you can achieve this. Some of them listed below.
If VC1 is your root VC for the navigation stack you can use the
below API in VC3
popToRootViewController(animated:)
Other way you can get the viewControllers array property from the
navigationController and get the VC1 from the array and use the
below API in VC3
popToViewController(_:animated:)
Hope this help. Please comment any doubt.
I currently have parental "menu" TableView with UINavigationBar and from each cell there is a segues by reference outlet to 3 similar Views with different information.
In each View there is a buttons to other 2 Views.
With every button's segue opens another View.
The problem:
From every View UINavigationBar's back button returns me to previous View but i tries to make back button to "menu".
Additional Bar Button Item and segue from it makes very close effect but segue animation is not like in UINavigationController.
How I could clean UINavigationBar transitions history in segue to initial View?
You can try pop to root view controller or You can edit navigation controller viewControllers property and remove/add some VC in between.
You can try Unwind Segue mechanism too.
Here are some methods(function) that navigation controller providing for pop operations. They are returning optional UIViewController (intance) from it’s navigation stack, that is popped.
open func popViewController(animated: Bool) -> UIViewController? // Returns the popped controller.
open func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? // Pops view controllers until the one specified is on top. Returns the popped controllers.
open func popToRootViewController(animated: Bool) -> [UIViewController]?
Here is sample code as a solution to your query::
// if you want to back to root of your app
if let rootNavigationController = self.window?.rootViewController as? UINavigationController {
rootNavigationController.popToRootViewControllerAnimated(true)
}
// But if you want to back to root of your current navigation
if let viewcontroller = self.storyboard?.instantiateViewController(withIdentifier: "NewViewController") as? NewViewController { // or instantiate view controller using any other method
viewcontroller.navigationController?.popToRootViewControllerAnimated(true)
}
Let's say that on my regular flow I have
VC1 -> VC2 -> VC3 -> VC4
But I have a requirement that when I touch a special button on VC1 it needs to go to VC4.
No problem there. The problem is that when I tap the back button on VC4, it needs to go back to VC3 instead of VC1.
I already tried pushing From VC1 -> VC2 and VC2 -> VC3 without animation and then VC3 -> VC4 with animation, but you can see a quick glimpse of VC3 which looks awful.
Any ideas?
I think in this specific case, you can insert a view controller on the navigation controllers stack after presenting the 4th viewController
if let navigationController = navigationController {
navigationController.pushViewController(vc4, animated: true)
let vc3Index = navigationController.viewControllers.count - 1
navigationController.viewControllers.insert(vc3, atIndex: vc3Index)
}
This should place VC3 next in line when the user presses back from VC4. Untested code, btw.
You can modify the UINavCon's viewControllers to achieve the order you want.
func specialButtonInVc1() {
self.navigationController?.pushViewController(fourth, animated: true)
self.navigationController?.viewControllers = [self, second, third,fourth]
}