Can i pop to Specific ViewController? - iphone

I am using navigation based application. I push First ViewController to Second ViewController and from Second ViewController to Third ViewController. Now I want to pop from Third ViewController to First ViewController.I am performing this task using the below code but my application crashed.
Please any body give me some proper guidelines. I can't use pop to rootViewController because it's different viewController. Thanks in advance...
In Third ViewControler i have written this:
FirstViewCtr *x=[[FirstViewCtr alloc] initWithNibName:#"FirstViewCtr" bundle:nil];
[self.navigationController popToViewController:x animated:NO];

By Writing the First Line you get the Indexes of all View Controllers and from second Line You will reach up to your Destination.
NSArray *array = [self.navigationController viewControllers];
[self.navigationController popToViewController:[array objectAtIndex:2] animated:YES];

A safer approach:
- (void)turnBackToAnOldViewController{
for (UIViewController *controller in self.navigationController.viewControllers) {
//Do not forget to import AnOldViewController.h
if ([controller isKindOfClass:[AnOldViewController class]]) {
[self.navigationController popToViewController:controller
animated:YES];
return;
}
}
}

Swifty way:
let dashboardVC = navigationController!.viewControllers.filter { $0 is YourViewController }.first!
navigationController!.popToViewController(dashboardVC, animated: true)

Swift 4 version
if let viewController = navigationController?.viewControllers.first(where: {$0 is YourViewController}) {
navigationController?.popToViewController(viewController, animated: false)
}
You may specify another filter on .viewControllers.first as per your need e.g lets say if you have same kind of view controllers residing in the navigation controller then you may specify an additional check like below
if let viewController = navigationController?.viewControllers.first(where: {
if let current = $0 as? YourViewController {
return current.someProperty == "SOME VALUE"
}
return false } ) {
navigationController?.popToViewController(viewController, animated: false)
}

Often it is more important to do that from top of stack, so:
- (void)popToLast:(Class)aClass
{
for (int i=self.navigationController.viewControllers.count-1; i>=0; i--)
{
UIViewController *vc = self.navigationController.viewControllers[i];
if ([vc isKindOfClass:aClass])
{
[self.navigationController popToViewController:vc animated:YES];
break;
}
}
}
and you call that
popToLast:[SomeViewController class];

- (void) RetunToSpecificViewController{
for (UIViewController *controller in self.navigationController.viewControllers)
{
if ([controller isKindOfClass:[AnOldViewController class]])
{
//Do not forget to import AnOldViewController.h
[self.navigationController popToViewController:controller
animated:YES];
break;
}
}
}

[self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:1] animated:YES];

Quick and safe Swift 3 version:
if let vc = navigationController.viewControllers.filter({ $0 is SpecificViewControllerClass }).first {
navigationController.popToViewController(vc, animated: true)
}

Your code creates a new instance of a view that has never been pushed onto the stack, then tries to pop back to that controller.
If you are popping back to the root view controller, you can uses popToRootViewControllerAnimated:
If you are popping back a known distance you can call popViewControllerAnimated: more than once. In your example, that would be 2 controllers so to calls. You could do the same thing by looking in viewControllers for the controller 2 from the end and popping to it.
The above suggestions are quick fixes. One best practice scenario would be to pass the controller you want to return to along to each successive controller you push. First passes itself to second, second passes that reference to third, third pops to the passed reference, which is first.
In effect, you are creating a temporary root controller. You could subclass UINavigationController and add a temporaryRoot property and a popToTemporaryRootViewControllerAnimated: method that would pop to your temporary root and clear it. When first pushes seconds, it would also set itself as the temporary root so that every controller in the stack does not have to pass a reference around. You would have to add some extra checks to unsure you never pop past the temporaryRoot without clearing it.

After lots of effort someone has created swift extension of back to a particular view controller in Swift 3.0.
extension UINavigationController {
func backToViewController(viewController: Swift.AnyClass) {
for element in viewControllers as Array {
if element.isKind(of: viewController) {
self.popToViewController(element, animated: true)
break
}
}
}
}
Method calling:
self.navigationController?.backToViewController(viewController: YourViewController.self)

Implemented & Tested in Swift 3.0
Below is Method which can useful for Navigate to any specific View Controller :
func poptoSpecificVC(viewController : Swift.AnyClass){
let viewControllers: [UIViewController] = self.navigationController!.viewControllers
for aViewController in viewControllers {
if aViewController.isKind(of: viewController) {
self.navigationController!.popToViewController(aViewController, animated: true)
break;
}
}
}
Usage :
self.poptoSpecificVC(viewController: createIntervalVC.self)

I think that .filter({...}).first is a little bit slower than .first(where: {...}).
Also this could be written more precisely to address only UIViewControllers.
extension UINavigationController {
func popToController<T: UIViewController>(_ type: T.Type, animated: Bool) {
if let vc = viewControllers.first(where: { $0 is T }) {
popToViewController(vc, animated: animated)
}
}
func popToControllerOrToRootControllerIfNotInTheStack<T: UIViewController>(_ type: T.Type, animated: Bool) {
if let vc = viewControllers.first(where: { $0 is T }) {
popToViewController(vc, animated: animated)
} else {
popToRootViewController(animated: animated)
}
}
}

Updated for Swift 3:
used below simple code, for pop to specific view controller;
for vc in self.navigationController!.viewControllers as Array {
if vc.isKind(of: YourViewControllerName) {
self.navigationController!.popToViewController(vc, animated: true)
break
}
}

for controller in self.navigationController!.viewControllers as Array {
if controller.isKind(of: LoginVC.self) {
_ = self.navigationController!.popToViewController(controller, animated: true)
break
}
}

Put function in UIViewController
1. it checks if Specific UIViewController exists In UINavigationController then popToViewController or else pushViewController
func navigate(_ navVC: AnyClass, pushVC: UIViewController) {
for obj in self.navigationController!.viewControllers {
if obj.isMember(of: navVC) {
self.navigationController!.popToViewController(obj, animated: true)
return
}
}
self.navigationController!.pushViewController(pushVC, animated: true)
}
Use
self.navigate(ViewController.self, pushVC: self.storyboard?.instantiateViewController(withIdentifier: "ViewController") as! ViewController)

i have answer here. This is 100% working code for Swift > 4.X
How can I pop specific View Controller in Swift

Related

Is it possible to observe changes in presentingViewController?

Is there any equivalent in Swift to RACObserve(self, presentingViewController)?
Or any other why to imitate this behaviour?
My issue is that I want to be notified whenever a view controller is "hidden" by another view controller. In objc what I'd do is to check if self.presentingViewController is nil.
Note that in this scenario there's no knowledge of which view controller is presented, so it's impossible to notify from within its viewDidAppear/viewDidDisappear.
As I understand your question: you need to to know which view controller is presented now and you need notification inviewDidAppear/viewDidDisappear.
So we can get this in several way.
The simple way is:
Get information of which is the top ViewController right now.
2.Call this method in your viewDidAppear/viewDidDisappear
Like this :
Get Which is The Top ViewController
func getTopViewController() -> UIViewController? {
if var topVC = UIApplication.shared.keyWindow?.rootViewController {
while let presentedViewController = topVC.presentedViewController {
topVC = presentedViewController
return topVC
}
return topVC
}
return nil
}
Call in viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if let top = getTopViewController() {
print("topView Controller name \(top.title)")
top.view.backgroundColor = .red
}
}
Hope it will help you !

How to detect if a pushed viewcontroller appears again?

assuming I have a viewcontroller (vcA) that pushes QRCodeScannerViewcontroller (vcB). When (vcB) scanned something, It will push ResultviewController (vcC).
-Those 3 views is connected to a UInavigation controller
-the user clicks on the back button on (vcC)
my question is:
1)how can I know if (vcB) is visible without changing code on (vcB)? (vcB) is a pod
2)where will I put this code? I can only access (vcA)
i tried adding this code on (vcA) but nothing happened
override func viewDidDisappear(_ animated: Bool) {
if (vcB.isViewLoaded && (vcB.view.window != nil)){
print("vcb did appear!")
}
}
To know if an instance of cvB's class exists in the navigation stack, you could use this piece of code:
let result = self.navigationController?.viewControllers.filter({
if let vcB = $0 as? UIViewController { // Replace UIViewController with your class, for example ViewControllerB
return true
}
return false
})
if result.isEmpty {
print("An instance of vcB's class hasn't been pused before")
} else {
print("An instance of vcB's class has been pused before")
}

Swift - How to pass UInavigationController and also pass variables

What is the best way to pass a UInavigationController and also pass variables to a new viewController. I know how to do one or the other but not both at the same time. Thank you in advance
this is my current code
func(){
let vc = storyboard?.instantiateViewControllerWithIdentifier("messagesViewController") as! UINavigationController
let posts = self.postList[indexPath.row]
//this is the var that i want to past
//vc.previousViewMessageId = posts.postKey
self.presentViewController(vc, animated: true, completion: nil)
}
If I understand you correctly, you have a view controller that can present a second VC. And this VC is embedded in a UINavigationController. What you don't know how to do, is to pass data from the first VC, to the navigation controller, then to the second VC.
Here is a brute force solution. It's not beautiful, but it works anyway.
Make your own UINavigationController subclass:
class DataPasserController: UINavigationController {
var previousViewMessageId: SomeType?
override func viewDidLoad() {
if let vc = self.topViewController as? YourSecondViewController {
vc.previousViewMessageId = self.previousViewMessageId
}
}
}
Now you can add a navigation controller in the storyboard, set its class to DataPasserController, and connect the second VC to it as its root view controller.
Now suppose you have got an instance of DataPasserController by calling instantiateViewControllerWithIdentifier, you can do this:
yourDataPasserControllerInstance.previousViewMessageId = posts.postKey
And present the instance!
To pass a value to your Navigation Controller's Root View Controller, you access viewControllers[0] and cast it to the class of your Messages View Controller (the controller that has the previousViewMessageId property):
func () {
let messagesNC = storyboard?.instantiateViewControllerWithIdentifier("messagesViewController") as! UINavigationController
let messagesVC = messagesNC.viewControllers.first as! MessagesViewController
messagesVC.previousViewMessageId = postList[indexPath.row].postKey
presentViewController(messagesNC, animated: true, completion: nil)
}
What you have there is simply presenting a view controller... You are skipping the navigation controller.
What you need to do is present the new view controller inside the navigation controller. Once you have done that, it will show correctly. You can also pass the variables after you've created the vc variable.
This presents the new viewController (vc) within the navigation controller...
self.navigationController?.pushViewController(vc, animated: false)
This sets the variable in the new viewController (vc) (you are correct)
vc.previousViewMessageId = posts.postKey
So complete:
func(){
let vc = storyboard?.instantiateViewControllerWithIdentifier("messagesViewController") as! MessagesViewController
let posts = self.postList[indexPath.row]
//this is the var that i want to past
vc.previousViewMessageId = posts.postKey
navigationController?.pushViewController(vc, animated: false)
}
PS. While not part of the question, I feel I should still mention... Use of the word self should be left to necessity only. In other words, don't use it when it isn't needed. for example self.postList[indexPath.row] :)
https://github.com/raywenderlich/swift-style-guide#use-of-self

How to tell if there is a modal UIViewController presented?

Is there a way to tell whether there is a modal UIViewController presented already, say, before calling dismissModalViewControllerAnimated?
iOS 9, 8, 7, 6 & 5
There are just too many answers to this question, none covering all cases. Furthermore, despite what you find in the documentation, there are two alternatives to the now deprecated modalViewController:
If you need to know if you are modal:
BOOL modal = nil != [self presentingViewController];
If you need to know if you are covered by a modal:
BOOL hiddenByModal = nil != [self presentedViewController];
iOS6+ - use presentedViewController:
Since iOS 6, presentedViewController should be used instead as the modalViewController which has been deprecated
Use the property:
Deprecated - modalViewController:
The controller for the active modal view—that is, the view that is temporarily displayed on top of the view managed by the receiver. (read-only)
#property(nonatomic, readonly) UIViewController *modalViewController
I usually add a BOOL variable, called something like isModal, and I set it after initializing a viewcontroller but before calling presentModalViewController. Something like:
MyViewController *controller = [[MyViewController alloc] init];
controller.isModal = YES;
[self presentModalViewController:controller animated:YES];
And then, in MyViewController, before needing to dismiss, I just check:
if (isModal) { //dismiss modal }
after iOS 5 you should use:
if (self.presentingViewController != nil) {
[self dismissViewControllerAnimated:YES completion:^{
//has dismissViewControllerAnimated
}];
}
Edited to change iOS Version
I understand this has been a while but just wanted do add my 2 cents to this matter.
I was in need to identify if there was a modally presented ViewController when the app went to background in order to dismiss it first.
First I made an extension of UIWindow to return me the current ViewController:
extension UIWindow {
func getCurrentViewController() -> UIViewController? {
guard let rvc = self.rootViewController else {
return nil
}
if let pvc = rvc.presentedViewController {
return pvc
} else if let svc = rvc as? UISplitViewController, svc.viewControllers.count > 0 {
return svc.viewControllers.last!
} else if let nc = rvc as? UINavigationController, nc.viewControllers.count > 0 {
return nc.topViewController!
} else if let tbc = rvc as? UITabBarController {
if let svc = tbc.selectedViewController {
return svc
}
}
return rvc
}
}
Then I went into appDelegate and added a test on applicationDidEnterBackground():
func applicationDidEnterBackground(_ application: UIApplication) {
if let vc = self.window?.getCurrentViewController() {
if vc.presentingViewController != nil {
vc.dismiss(animated: false, completion: nil)
}
}
}
This solution is in Swift 3

Checking if a UIViewController is about to get Popped from a navigation stack?

I need to know when my view controller is about to get popped from a nav stack so I can perform an action.
I can't use -viewWillDisappear, because that gets called when the view controller is moved off screen for ANY reason (like a new view controller being pushed on top).
I specifically need to know when the controller is about to be popped itself.
Any ideas would be awesome, thanks in advance.
Override the viewWillDisappear method in the presented VC, then check the isMovingFromParentViewController flag within the override and do specific logic. In my case I'm hiding the navigation controllers toolbar. Still requires that your presented VC understand that it was pushed though so not perfect.
Fortunately, by the time the viewWillDisappear method is called, the viewController has already been removed from the stack, so we know the viewController is popping because it's no longer in the self.navigationController.viewControllers
Swift 4
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let nav = self.navigationController {
let isPopping = !nav.viewControllers.contains(self)
if isPopping {
// popping off nav
} else {
// on nav, not popping off (pushing past, being presented over, etc.)
}
} else {
// not on nav at all
}
}
Original Code
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if ((self.navigationController) &&
(![self.navigationController.viewControllers containsObject:self])) {
NSLog(#"I've been popped!");
}
}
Try overriding willMoveToParentViewController: (instead of viewWillDisappear:) in your custom subclass of UIViewController.
Called just before the view controller is added or removed from a container view controller.
- (void)willMoveToParentViewController:(UIViewController *)parent
{
[super willMoveToParentViewController:parent];
if (!parent) {
// `self` is about to get popped.
}
}
I don't think there is an explicit message for this, but you could subclass the UINavigationController and override - popViewControllerAnimated (although I haven't tried this before myself).
Alternatively, if there are no other references to the view controller, could you add to its - dealloc?
This is working for me.
- (void)viewDidDisappear:(BOOL)animated
{
if (self.parentViewController == nil) {
NSLog(#"viewDidDisappear doesn't have parent so it's been popped");
//release stuff here
} else {
NSLog(#"PersonViewController view just hidden");
}
}
You can catch it here.
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (viewController == YourAboutToAppearController) {
// do something
}
}
This will fire just before the display of the new View. Nobody's moved yet. I use all the time to do magic in front of the asinine NavigationController. You can set titles and button titles and do whatever there.
I have the same problem. I tried with viewDisDisappear, but I don't have the function get called :( (don't know why, maybe because all my VC is UITableViewController).
The suggestion of Alex works fine but it fails if your Navigation controller is displayed under the More tab. In this case, all VCs of your nav controllers have the navigationController as UIMoreNavigationController, not the navigation controller you have subclassed, so you will not be notified by the nav when a VC is about to popped.
Finaly, I solved the problem with a category of UINavigationController, just rewrite - (UIViewController *)popViewControllerAnimated:(BOOL)animated
- (UIViewController *)popViewControllerAnimated:(BOOL)animated{
NSLog(#"UINavigationController(Magic)");
UIViewController *vc = self.topViewController;
if ([vc respondsToSelector:#selector(viewControllerWillBePopped)]) {
[vc performSelector:#selector(viewControllerWillBePopped)];
}
NSArray *vcs = self.viewControllers;
UIViewController *vcc = [vcs objectAtIndex:[vcs count] - 2];
[self popToViewController:vcc animated:YES];
return vcc;}
It works well for me :D
I tried this:
- (void) viewWillDisappear:(BOOL)animated {
// If we are disappearing because we were removed from navigation stack
if (self.navigationController == nil) {
// YOUR CODE HERE
}
[super viewWillDisappear:animated];
}
The idea is that at popping, the view controller's navigationController is set to nil.
So if the view was to disappear, and it longer has a navigationController, I concluded it was popped. (might not work in other scenarios).
Can't vouch that viewWillDisappear will be called upon popping, as it is not mentioned in the docs. I tried it when the view was top view, and below top view - and it worked in both.
Good luck,
Oded.
Subclass UINavigationController and override popViewController:
Swift 3
protocol CanPreventPopProtocol {
func shouldBePopped() -> Bool
}
class MyNavigationController: UINavigationController {
override func popViewController(animated: Bool) -> UIViewController? {
let viewController = self.topViewController
if let canPreventPop = viewController as? CanPreventPopProtocol {
if !canPreventPop.shouldBePopped() {
return nil
}
}
return super.popViewController(animated: animated)
}
//important to prevent UI thread from freezing
//
//if popViewController is called by gesture recognizer and prevented by returning nil
//UI will freeze after calling super.popViewController
//so that, in order to solve the problem we should not return nil from popViewController
//we interrupt the call made by gesture recognizer to popViewController through
//returning false on gestureRecognizerShouldBegin
//
//tested on iOS 9.3.2 not others
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let viewController = self.topViewController
if let canPreventPop = viewController as? CanPreventPopProtocol {
if !canPreventPop.shouldBePopped() {
return false
}
}
return true
}
}
You can use this one:
if(self.isMovingToParentViewController)
{
NSLog(#"Pushed");
}
else
{
NSLog(#"Popped");
}
You can observe the notification:
- (void)viewDidLoad{
[super viewDidLoad];
[NSNotificationCenter.defaultCenter addObserver:self selector:#selector(navigationControllerWillShowViewController:) name:#"UINavigationControllerWillShowViewControllerNotification" object:nil];
}
- (void)navigationControllerDidShowViewController:(NSNotification *)notification{
UIViewController *lastVisible = notification.userInfo[#"UINavigationControllerLastVisibleViewController"];
if(lastVisible == self){
// we are being popped
}
}
I needed to also prevent from popping sometimes so the best answer for me was written by Orkhan Alikhanov. But it did not work because the delegate was not set, so I made the final version:
import UIKit
class CustomActionsNavigationController: UINavigationController,
UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
override func popViewController(animated: Bool) -> UIViewController? {
if let delegate = topViewController as? CustomActionsNavigationControllerDelegate {
guard delegate.shouldPop() else { return nil }
}
return super.popViewController(animated: animated)
}
// important to prevent UI thread from freezing
//
// if popViewController is called by gesture recognizer and prevented by returning nil
// UI will freeze after calling super.popViewController
// so that, in order to solve the problem we should not return nil from popViewController
// we interrupt the call made by gesture recognizer to popViewController through
// returning false on gestureRecognizerShouldBegin
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let delegate = topViewController as? CustomActionsNavigationControllerDelegate {
if !delegate.shouldPop() {
return false
}
}
// This if statement prevents navigation controller to pop when there is only one view controller
if viewControllers.count == 1 {
return false
}
return true
}
}
protocol CustomActionsNavigationControllerDelegate {
func shouldPop() -> Bool
}
UPDATE
I have added viewControllers.count == 1 case, because if there is one controller in the stack and user makes the gesture, it will freeze the UI of your application.
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
const BOOL removingFromParent = ![self.navigationController.viewControllers containsObject:self.parentViewController];
if ( removingFromParent ) {
// cleanup
}
}
Maybe you could use UINavigationBarDelegate's navigationBar:shouldPopItem protocol method.
Try making this check in viewwilldisappear
if ([self.navigationController.viewControllers indexOfObject:self] == NSNotFound) {
//popping of this view has happend.
}