Last night and today I was running into an issue where I wanted to jump from one position in an iOS app back to the start. Essentially I had something that looked a lot like this:
Several View Controllers lined up in a row. In this case, the view controller at the far left (so the second “item”) is the root. There is a button on that VC that pushes you to the second (or middle item). The second VC has another button that pushes you to the third VC (fourth item). That VC has two buttons: one that should return you back to the beginning and one that opens another VC in a modal. That last VC has a single button which should also push you back to the beginning. I did it this way so I could test out both returning from a modal and returning through a pushed stack of VCs. Basically, I wanted to be able to do this:
This isn’t very uncommon and a quick search of the internet will review quite a few posts on Stack Overflow and other sites. It’s important to point that here I’m going to each “next” view controller via a segue. Now the “old” or common way of returning back definitely wasn’t as nice as just going back via a segue or anything like that. The steps you used to take were to:
- Create a delegate on the destination View Controller.
- Implement that delegate on the source View Controller.
- In the prepareForSegue method, set the destination VC’s delegate to be the source VC.
- When you want to “bounce back” you’d have to keep firing methods up the delegate ladder until you got to where you wanted to be.
I don’t want to say that I’m lazy but putting all of this code into a sample didn’t make much sense. It clutters up the code and takes the focus away from what I want to actually demonstrate. I put a question out on Twitter asking how people would suggest pulling this off and received one interesting response from my fellow polyglot Sasha. His suggestion was to use something called an unwind segue. Right before he posted that response, I actually found a different, fairly straightforward, solution so I figured I would post about them both. I ran into a funny issue with the unwind segue that I didn’t find documented anywhere so I thought it would be a smart idea to put it up here.
Doing it in code
The first technique I found for doing this came about as I was messing around with different ways to pop and dismiss view controllers. Essentially what I wanted to do was return to the root view controller and from any controller, it’s possible to access the root VC. From there you just need a way to dismiss any modals and then pop any VCs on top of the navigation stack. Let’s take a look at how you do this by first looking at the source view controller (i.e. one of the VCs on the right):
Here we’re getting a reference to the root UINavigationController. After that we access the actual Root View Controller. Lastly, we say we want to call the returnToRoot method on the root view controller using the performSelector method. Now we can hop over to the root VC and implement that method:
The first thing we do is call dismissViewController on the VC which will remove any modals. Next, we call the popToRootViewControllerAnimated method on the VC’s navigationController property. This removes all of the pushed VCs from the navigation stack. This all seems to work fine. I will point out that this might not work if you have several modals mixed in with different NavigationControllers. Though if you are doing that, you probably have a bigger problem on your hands.
Unwind Segues
Prior to Sasha mentioning it in response to my tweet, I’m pretty sure I had not heard the term Unwind Segue before. I’m pretty happy I have now. I had to do a little looking online before I was pointed at a video from WWDC 2012 (session 407 if you have access to watch). In that video they talk mostly about storyboards and combining storyboards with XIB files. However, for the last 10 or so minutes, they cover a new to iOS 6 concept called Unwind Segues. The idea is that instead of you needing to do the delegate ladder steps mentioned above, Interface Builder can take care of it for you. This is done by doing two things. First you need to add a method to your destination VC. So for example, in the layout above, I’m returning to my root which is tied to the ViewController class (the same class you get with any default single page application). So, inside that class I’ve added this method:
This method returns an IBAction and takes in a UIStoryboardSegue. Using this method format will tell Xcode that this is a method that can be unwound to. Inside of that method I can cast the source VC from the segue object. So if there was information on the source VC that I want to pull out, I can do so inside of my reset method here. As it is, I’m not doing anything, just adding the empty method. The next step happens in the storyboard so open that. From the button I want to use to return to the first VC, I control + click and drag from the button to the green Exit icon below my view controller:
When I let go on Exit, I’ll see all of the methods in my code base that match the format I mentioned above:
Here I can select the reset method I created before. Now when I run my app and tap on the button I connected, iOS will look back through my stack and call a method named canPerformUnwindSegueAction on each VC up the stack until it finds one that will respond. So in other words, it’s going to travel up the stack and say “can this view controller respond to the reset selector? No? ok I’ll keep going. Yes? Ok I’ll execute it and remove the other VCs in between!”. So when I tap the button, it will go back to the first VC.
What’s the catch?
When I did this with the simple app I outlined above, everything worked fine. When I fired my reset Exit method from the VCs on the right side, things were returned to my root VC. All was good (and arguably better than what I outlined above). However, when I implemented the same thing in my more complex sample, it didn’t work. At first I thought it had something to do with the fact that I was trying to unwind from a Modal down through some pushed VCs to the root. However, even when I tried from somewhere that was just 3 pushed VCs in (i.e. no Modals) it didn’t work. In fact, each time, whether I was exiting from a modal or a pushed VC, only one VC was dismissed or popped. I was racking my brains until I thought, maybe I need to implement the canPerformUnwindSegueAction myself. So sure enough, after adding this method to all of the custom View Controllers I had implemented “in between” and returned NO (i.e. this VC won’t respond to the reset selector) things worked and it returned to the root VC properly.
Conclusion
Today we looked at two different methods to return to a root view controller from somewhere down the navigation stack in your iOS apps. The first technique was all code based and required us calling a method on the root VC which then dismisses any models and then popping any pushed VCs. The second method uses a new technique called Unwind Segues which enables you to connect an action in Interface Builder to a Exit method in another VC. iOS will then handle traversing the stack and figuring out what VC to return to (with potentially a little help from you and the canPerformUnwindSegueAction method). Remember that the Unwind Segues are iOS 6 and up. Hopefully this helps some developers out there as much as it helped me.
18 Comments
Rob
Great explanation, i also found a way to unwind from code (instead of hooking up a botton to the exit icon), perhaps you could add it too.
Chris
What was the way you found?
zemorango
[self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:1] animated:YES];
Chris
Will that work for Modals as well or only items on the UINavigationController stack?
Clint
Thanks for the post. This was a big help in solving a problem for me.
Chris
Happy it helped!
Igor Hasenheim
This unwinding is included in the apple-online tutorial for beginners and I'm kind of stuck there. It is set modal and I have the controller subclasses and everything, but I can't draw from a button to that "exit" box. It is possible to draw from the button to the box "first responder" or to any other view/navigation controller though. Any hint?
Chris
Not that I'm aware of. Did you add the reset method already as I don't think you can drag to the Exit button if you haven't already done that.
Igor Hasenheim
i had implemented the method etc., but it didn't work out in xcode 4.52. now i changed to xcode 5 and it works.
Adam
What if you are unwinding from a child navigation controller and want to go back to parent navigation controller? When I'm in child navigation controller, popping to root view only takes me back to the root view of the CHILD navigation controller. I want to go back to the parent.
Chris
I think unwinding should work in this situation where as it would be very complicated to handle it in code (popping, and then having the vc you returned to pop itself and so on).
alistar
thanks for the info, I served for the application that I am developing, by the way, can also be applied between two storyboards, Greetings
Lộc Đ. Phạm
Thanks Chris for the tutorial. When I unwind from the last VC to the first VC, the UIViewController dealloc: methods in the middle VC do not get invoked. Do you know what maybe the problem?
Arron Ferguson
Whoa. Thanks! This helped me immensely, that's awesome!
origds
It's possible to do a unwind segue for two UIViewControllers that are not from the same UINavigationController?
I was trying this, but it does not work, the error it throws is that UIViewController doesn't belong to the same UINavigationController
Chris
Sounds like you'd need two different unwind segues for this.
Guest
Could you explain me more about how to use two differents unwind segues?
I attach a picture, showing my storyboard, I want to do the unwind segue between Period and Batch, for go back.
Thanks in advance.
origds
I don't know why the question seems like a Guest question, but it's mine.