Intercepting ios back button to send back to base view controller - ios

We have a sequence of screens that navigate in order...
BaseViewController -> A -> B -> BaseViewController
Each screen uses NavigationController.PushViewController to go from...Base->A, A->B. So each subsequent screen is placed in the navigation stack.
So if you are on A and you click 'back', then it goes back one screen. This works well as it is controlled by the NavigationController.
However, when you are on screen B, 'back' should to back to the BaseViewController.
Instead it goes (as designed by Apple) back to A. Is there a way to intercept the 'back' button on B so we can instead use NavigationController.PopToViewController to send the user back to BaseViewController?

As #the4kman mentioned , we can create a custom button to replace the LeftBarButtonItem ,and handle the back event .
ViewDidLoad in B
this.NavigationItem.LeftBarButtonItem =
new UIBarButtonItem("back", UIBarButtonItemStyle.Plain, (sender,e) => {
UIViewController baseVC = NavigationController.ViewControllers[NavigationController.ViewControllers.Length - 3];
NavigationController.PopToViewController(baseVC, true);
});
As #J.C. Chaparro mentioned , remove A from stack .
ViewDidLoad in B
List<UIViewController> list = NavigationController.ViewControllers.ToList<UIViewController>();
list.RemoveAt(list.Count-2);
NavigationController.ViewControllers = list.ToArray();

You can do something like this in B's viewDidAppear function:
guard let navigationController = self.navigationController else {
return
}
navigationController.viewControllers.remove(at: navigationController.viewControllers.count - 2)
This will remove A from the stack, and allow you to go back to BaseViewController from B.

If I understand correctly you want to pop to root view controller from a certain top view controller. One way to do it would be to create a subclass of UINavigationController and override popViewController method where you would check what you have on top at the moment and decide to pop to root or not. Here's an example:
open class CustomNavigationController: UINavigationController {
override open func popViewController(animated: Bool) -> UIViewController? {
if topViewController is BViewController {
return popToRootViewController(animated: animated)?.last
} else {
return super.popViewController(animated: animated)
}
}
}

Approach:
Use child view controllers
Steps:
Create a view controller C which is a subclass of Base
Add A as a child view controller to C
Create a view controller D which is a subclass of Base
Add B as a child view controller to D
Push C to D

Thanks for all the answers. #Cole Xia put me on the right path for our scenario. His technique works, and the following works as well.
Here is the Xamarin code. The technique is to replace the current list of ViewControllers with a new one. Then when 'back' is hit on B, it goes right back to BaseViewController.
var viewControllers = new UIViewController[] { NavigationController.ViewControllers[0], new B() };
NavigationController.SetViewControllers(viewControllers, false);

Related

How to close multiple viewcontrollers in one time?

For my requirement, I have to go back to my rootviewcontroller (tabbar) but It have many page present on it.
example flow
my tabbar (have 4 tabs each tab has own navigation ) -> push(vc1) -> present nav(vc2) -> push vc3 -> present nav(vc4)
If I want to close all viewcontroller ( vc1 - vc4 ) How to dismiss them by one function ?
You can use UIViewController.dismiss(animated:completion:) call on the base view controller that started presentation from tab bar (root level).
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.
Given this hierarchy
my tabbar (have 4 tabs each tab has own navigation ) -> push(vc1) -> present nav(vc2) -> push vc3 -> present nav(vc4)
You can walk back the presentation hierarchy like following -
let vc4Presenter = vc4.navigationController?.presentingViewController
let vc2NavCtrl = (vc4Presenter as? UINavigationController) ?? vc4Presenter?.navigationController
let vc2Presenter = vc2NavCtrl?.presentingViewController
let vc1NavCtrl = (vc2Presenter as? UINavigationController) ?? vc2Presenter?.navigationController
vc2Presenter?.dismiss(animated: true, completion: {
vc1NavCtrl?.popToRootViewController(animated: false)
})
Above is merely an example of how you can find the correct view controller instance to call dismiss on in the view hierarchy. This is definitely not well suited for dynamic number of presentation layers.
You can -
Write this in a recursive way (so that it keeps looking for presentingViewController until it finds nil for the root level).
Have a convenient reference to tab bar controller throughout the app and call dismiss on it's currently selected view controller (tab).
If you want go directly to first viewController:
self.popToRootViewController(animated: true)
If you want to see how it is going away one by one
#objc func buttonPressed() {
let viewControllers: [UIViewController] = self.viewControllers
for aViewController in viewControllers {
self.popViewController(animated: true)
}
self.popToRootViewController(animated: true)
}

Double return to previous view - Swift

I'm new with IOS and Swift so don't judge if solution is easy.
I have three ViewControllers like A,B and C.
I started from A -> NavigationController -> B -> NavigationController -> C
In specific situation I need to come back from C to A without seeing B. Is any way to do this?
Maybe changing the parent navigationController? Maybe I can print stack with every current view? - it will be really helpful.
I tried dismiss C and B view one by one and it work's but then we can see B view for a moment - so it's not a solution for me.
P.s : I'm using Modal kind to switch between controllers.
enter image description here
If A is always the first view controller, you can just do :
viewcontrollerC.navigationController?.popToRootViewController(animated: true)
This methods pop the stack to the first view controller, without displaying intermediates ones
If A is not the first viewController, you can do :
viewcontrollerC.navigationController?. popToViewController(viewControllerA, animated: true)
If you don't have a reference to viewControllerA, search it in the stack :
let viewControllerA: UIViewController?
for (let vc in (self.navigationController?.viewControllers ?? [])) {
//adust the test to find the appropriate controller
if vc.isKindOf(ViewControllerAClass.self) {
viewControllerA = vc
break
}
}
if let viewControllerA = viewControllerA {
self.navigationController?.popToViewController(viewControllerA, animated: true)
}
source : https://developer.apple.com/documentation/uikit/uinavigationcontroller/1621871-poptoviewcontroller
There are 2 ways you can achieve this. The simple to implement is in View Controller C you can, on in the specific situation, invoke following function:
navigationController?.popToRootViewController(animated: true)
This will pop all the navigational view hierarchy and take you back to the root i.e. the first view controller.
Second approach is to define unwind method in the view controller you want to go back to. In view controller when you start typing unwind, in Xcode 10 you will get autocomplete to add this Swift Unwind Segue Method.
#IBAction func unwindToA(_ unwindSegue: UIStoryboardSegue) {
let sourceViewController = unwindSegue.source
// Use data from the view controller which initiated the unwind segue
}
In this particular question let us say you added this method in View Controller A as you want to go back to it. I assume you have a button on View Controller C to go back to A. Controll+Drag from the button to the Exit symbol of the view controller A. The unwindToA method will automatically pop-up. Connect to it and you are done. When the user presses this button it will go back 2 navigation controllers to A.
Note: By this method you can go back to any navigation controller on the Navigation stack and it is not limited to root view controller alone. Below I am addition picture showing the exit on a view controller.

Unwind multiple navigation controllers

Say I have view controllers A, B & C embedded in a navigation controller. From C, I present a new navigation controller for a separate logical flow but need to return back to A upon completion. So the app flow is as follows: A->B->C -- present new navigation controller modally -- D->E->F. Then go from F back to A.
I have set up an unwind segue, however, the unwind segue only takes me back to D even though I have set it up to return back to A.
How can I make it unwind all the way back to A? Am I missing something I don't see? Thank you.
Inside A:
#IBAction func unwindToHome(segue:UIStoryboardSegue){}
Then I have control-dragged from F to its 'exit' and chose the unwind segue I created in A, and wrote this segue code:
private let SEGUE_TO_HOME = "unwindToHome"
performSegue(withIdentifier: SEGUE_TO_HOME, sender: nil)
When you unwind, it should go back up the chain of view controllers, through all of those navigation controllers and modal presentations, to get all the way to the view controller that has this unwind action implemented.
Is it possible that unwindToHome occurs in any of these view controllers other than A? That's the only way I can see that the unwind action wouldn't sent you all the way back to A. I'm wondering if, for example, D has its own unwindToHome action (or perhaps is another instance of the same type as A). Bottom line, I cannot reproduce the behavior you describe except through something like that.
You subsequently asked:
I have put a deinit method in all of the above view controllers. I only print the word gone in all of them. when it unwinds back to A, 'gone' is only printed twice. Doesn't the unwind segue deallocate all instances?
Yes, they should all be deallocated. If not, the “debug memory graph” (see https://stackoverflow.com/a/30993476/1271826) is excellent at showing what is keeping a strong reference to the ones that are not deallocated. Most likely, these un-deallocated view controllers have some lingering strong reference cycle, repeating timer reference, or something like that which is keeping a strong reference to each, respectively.
You could simply create a reference to the first navigation controller that controllers A-C are embedded in by creating a subclass for the second nav controller.
class SecondNavController: UINavigationController {
// getter for previous nav controller
var prevNavController: UINavigationController? {
return parent?.navigationController
}
}
and when you need to unwind, simply:
class FController: UIViewController {
// other code
func unwindToRootController() {
guard let navController = navigationController as? SecondNavController,
let prevNavController = navController.prevNavController else {
return
}
navigationController.popToRootViewController(animated: true)
}
}

UINavigationController popToRootViewController method cannot be called by a delegate?

I have a UINavigationController which works great. Each view controller has its own button that pops the stack back to its root which also works great. However, I'd like to also be able to pop the stack back to its root by pressing a button on the tab bar (which is obviously in an entirely different class outside of the navigation stack).
Therefore, I created a delegate in the tab bar class which finds the view controller at the top of the stack and calls the method in that view controller to pop the stack back to the root. I printed something to the console to verify that the delegate is set up correctly and it is. Everything works exactly as it should, except that pressing the tab bar doesn't pop the stack back to its root.
Thoughts?
This is the view controller at the top of a UINavigationController stack
class BlankViewController202: UIViewController, MainContainerViewControllerDelegate {
// pop to root
func popToRoot() {
self.navigationController?.popToRootViewController(animated: true)
print("success")
}
}
When this function above is called from within the view controller (when the user presses the button on the view controller itself), it pops the stack. But when this same exact method is called by a delegate from the tab bar, it doesn't pop the stack (but it does print to console so I know its hooked up properly).
This is where the button resides in the tab bar that when pressed should pop the stack back to its root
protocol MainContainerViewControllerDelegate {
func popToRoot()
}
class MainContainerViewController: UIViewController {
func moveToTab3(sender: UIButton!) {
// ...
let banana = BlankViewController202()
self.delegate = banana
delegate?.popToRoot()
}
}
The problem is that BlankViewController202() makes a whole new, separate BlankViewController202 — it is not the particular BlankViewController202 that is already in the interface as part of the navigation controller interface. It is that BlankViewController202 you want to talk to.
I think, you error delegate pattern. You can see again model delegate use protocol. If you use protocol, you delete line code "let banana = BlankViewController202()".
Just Follow some steps
1) Make one Object of UINavigationViewController in AppDelegate and you can access it with shared object of app delegate.
2) The first line of moveToTab3 will be [Appdelegate sharedObject].navigationViewControllerVariable = self.navigationViewController
3) In Your delegate method write this line
[[Appdelegate sharedObject].navigationViewControllerVariable popToRootViewController:true]
this will work definitely :)

Swift: How to segue between view controllers and use navigation bar to go backwards within a child view controller (XLPagerTabStrip)

I am currently implementing the XLPagerTabStrip (https://github.com/xmartlabs/XLPagerTabStrip) which effectively creates a tab bar at the top of the view controller. I want to be able to segue to a new view controller from one of the tabbed controllers and be able to use the navigation bar to move backwards (or a custom version of the navigation bar if this isn't possible).
XLPagerTabStrip provides the moveToViewController and moveToViewControllerAtIndex functions to navigate between child view controllers, but this method doesn't allow use of a navigation bar to go backwards.
Conceptually XLPagerTabStrip is a collection of view controllers declared and initialized during the XLPagerTabStrip model creation.
It has virtually no sense to use a UINavigationController if you already have all the viewcontrollers available.
You can create a global var previousIndex to store the previous viewController index and allow users to go back by using canonical methods:
func moveToViewControllerAtIndex(index: Int)
func moveToViewControllerAtIndex(index: Int, animated: Bool)
func moveToViewController(viewController: UIViewController)
func moveToViewController(viewController: UIViewController, animated: Bool)
About a new viewController, suppose you have 4 viewControllers that built your container (XLPagerTabStrip) named for example z1, z2, z3 e z4.
You can embed to z4 a UINavigationController (so it have the z4 controller as rootViewController) and start to push or pop your external views. When you want to return to your z4 you can do popToRootViewControllerAnimated to your UINavigationController
When you are go back to z4 , here you can handle your global var previousIndex to moving inside XLPagerTabStrip.
I'm not familiar with XLPagerTabStrip, but I had a similar problem recently and the solution was to use an unwind segue to go back to the previous view controller. It's pretty trivial to implement so probably worth a try.
To navigate back to your previous view tab controller, you had initially navigated from;
Embed your new view controller, from which you wish to navigate
away from in a navigation bar
Connect it's Navigation Bar Button to the Parent view containing the
tab bar by dragging a segue between the 2 views
Create a global variable in App delegate to store current index
which you will use in the Parent view to determine what tab view
controller to be shown
var previousIndex: Int = 0 //0 being a random tab index I have chosen
In your new view controller's (the one you wish to segue from)
viewdidload function, create an instance of your global variable as
shown below and assign a value to represent a representative index
of the child tab bar view controller which houses it.
//Global variable instance to set tab index on segue
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.previousIndex = 2
You can write this for as many child-tab connected views as you wish, remembering to set the appropriate child-tab index you wish to segue back to
Now, create a class property to reference your global variable and a function in your Parent view as shown below
let appDelegatefetch = UIApplication.shared.delegate as! AppDelegate
The function
func moveToViewControllerAtIndex(){
if (appDelegatefetch.previousIndex == 1){
self.moveToViewControllerAtIndex((self.appDelegatefetch.previousIndex), animated: false)
} else if (appDelegatefetch.previousIndex == 2){
self.moveToViewControllerAtIndex((self.appDelegatefetch.previousIndex), animated: false)
}
}
You may now call this function in the Parent View Controller's viewDidLoad, as shown below.
moveToViewControllerAtIndex()
Run your project and that's it.

Resources