Unwind multiple navigation controllers - ios

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

Related

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.

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.

Pass data between three viewController, all in navigationController, popToRootView

The issue I'm having is this.
I have a navigation controller with 3 viewController. In the 1st controller, I have the user select an image. This image is passed to 2nd and 3rd controller via prepareForSegue.
At the 3rd controller, I have a button that takes the user back to the 1st view controller. I explored 2 ways in doing this:
1) use performSegue, but I don't like this because it just push the 1st controller to my navigation stack. So I have this weird "Back" button at the 1st Viewcontroller now, which is not what I want. I want the app to take user directly to 1st viewcontroller without the back button.
2) I tried Poptorootviewcontroller. This solves the issue of the "back" button. But, when I pop back to the 1st viewcontroller, the user's selected image is still on screen. I want to clear this image when the user goes from the 3rd viewcontroller back to the 1st viewcontroller.
So with approach 2), how do I make sure all memory is refreshed and the image becomes nil in the 1st viewcontroller? Since I'm not using performSegue, 3rd viewcontroller does not have access to the 1st Viewcontroller.
For refresh, you'd have to clear it in viewWillAppear but I find this rather dangerous. Best you can do there is to create a new copy of the view controller everytime and Swift will take care of the rest. I don't know if you are using the storyboard but I would recommend using the class UIStoryboard and the function instiantiateViewControllerWithIdentifier("something") as! YourCustomVC
As long as you stay in the navigation stack, you'll not lose any of the current configurations of previous View Controllers.
As for passing data back to the first controller. You can either just throw it in the global scope which is the easiest way but might be difficult to know when it was updated or if the data is fresh. But you can always just:
var something: String = ""
class someView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
something = "foo"
}
}
Something will be availabe everywhere then.
You could make a protocol and pass the delegate along the 3 view controllers. So when you are starting it you could do:
func someAction() {
let v = SomeViewController()
v.delegate = self
self.navigationController?.pushViewController(v, animated: true)
}
And then with each following view:
func someOtherAction() {
let v = SomeOtherViewController()
v.delegate = self.delegate
self.navigationController?.pushViewController(v, animated: true)
}
Although personally I find it hard to keep track of this.
Lastly you could use the NSNotificationCenter to pass an object along with all the data and catch it in a function on your first controller.
To do this you first register your VC for the action in viewDidLoad() or something:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "someAction:", name: "someNotification", object: nil)
Then when you are done in the 3rd view make some object or a collection of string and send it back as follows:
NSNotificationCenter.defaultCenter().postNotificationName("someNotification", object: CustomObject())
And then lastly you'll catch it in the function "someAction"
func someAction(note: NSNotification) {
if let object = note.object as? CustomObject {
//Do something with it
}
}
Hope this helps!
Use an unwind segue which provides the functionality to unwind from the 3rd to the 1st (root) view controller.
The unwind segue is tied to an action in the root view controller. Within this action, you simply nil the image:
#IBAction func unwindToRootViewController(sender: UIStoryboardSegue)
{
let sourceViewController = sender.sourceViewController
// Pull any data from the view controller which initiated the unwind segue.
// Nil the selected image
myImageView.image = nil
}
As you can see in the action, segues also let you pass data back from the source view controller. This is a much simpler approach than needing to resort to using delegates, notifications, or global variables.
It also helps keep things encapsulated, as the third view controller should never need to know specifics about a parent view controller, or try to nil any image that belongs to another view controller.
In general, you pass details to a controller, which then acts on it itself, instead of trying to manipulate another controller's internals.

Dismiss view controller from unwind segue

I have 3 view controllers. I can go from A to B, then from B to C. I've made an unwind segue from B to A. When it's performed, I can see that view controller B has been deallocated, and VC A appears on screen.
To make it I added next code to VC A and made unwind segue on button in VC B.
#IBAction func unvindSegueToMenu(segue:UIStoryboardSegue) {}
However, when I go to VC C (from B) I want to perform unwind Segue from VC C to A. To do that I put next code to VC B class.
#IBAction func unvindSegueToMainMenu(segue:UIStoryboardSegue) {
self.dismissViewControllerAnimated(false, completion: nil)
}
After I press button in VC C, it shows me VC A as I want, but in Allocation Summary I can see that VC B has not been deallocated.
What is my mistake and how can I deallocate it?
Normally you made something wrong in your code.
i tested and i can see clearly that VC B is deallocated after performing the unwind segue.
First be sure that you made it right. (for example the performed method of the unwind segue should be implemented in the destination VC)
Also you can check after performing the unwind how many viewControllers you have in the navigationController..in your case should be 2 , but if you do it in the right way it will be only one.

Change view controller maintaining navigation hierarchy

Using the diagram below as a reference, I'm trying to go from controller A to controller B-2 when the users pushes a button.
I'm fairly new to iOS so I'm having some problems.
If I push the B-2 controller or create a segue the hierarchy is not respected and the B-2 controller is not inside the navigation controller (B).
I want the user to still have the navigation funcionalities and to still be able to go back to controller B-1 using the back button.
Controllers A & B are both under the same Tab controller (Z)
How can I achieve this?
In the end I found this solution:
From controller A I get the parent (Z), set the active controller to the second child (B) and get his child (B-1).
Once I have B-1 I can call perform segue and it works pretty good.
The hierarchy is correct and it's exactly the behaviour I wanted
The code is this
if let tab = self.parentViewController as? UITabBarController{
tab.selectedIndex = 1
if let one = tab.childViewControllers[1] as? UINavigationController{
if let two = one.childViewControllers[0] as? MatchTableViewController{
two.performSegueWithIdentifier("open_chat_view", sender: matchModel)
}
}
}

Resources