How can I pop specific View Controller in Swift - ios

I used the Objective-C code below to pop a specific ViewController.
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;
}
}
How can I do that in Swift?

Try following code:
for controller in self.navigationController!.viewControllers as Array {
if controller.isKind(of: ViewController.self) {
self.navigationController!.popToViewController(controller, animated: true)
break
}
}

Swift 5
To pop to the latest instance of a specific class, for example SomeViewController:
navigationController?.popToViewController(ofClass: SomeViewController.self)
But you need to add ths UINavigationController extension:
extension UINavigationController {
func popToViewController(ofClass: AnyClass, animated: Bool = true) {
if let vc = viewControllers.last(where: { $0.isKind(of: ofClass) }) {
popToViewController(vc, animated: animated)
}
}
}

For Swift 3+
let viewControllers: [UIViewController] = self.navigationController!.viewControllers
for aViewController in viewControllers {
if aViewController is YourViewController {
self.navigationController!.popToViewController(aViewController, animated: true)
}
}

From Swift 4.0 and Above
for controller in self.navigationController!.viewControllers as Array {
if controller.isKind(of: DashboardVC.self) {
_ = self.navigationController!.popToViewController(controller, animated: true)
break
}
}
This is working Perfect.

I prefer a generic way to do it.
I have this extension for the UINavigationController :
extension UINavigationController {
func backToViewController(vc: Any) {
// iterate to find the type of vc
for element in viewControllers as Array {
if "\(element.dynamicType).Type" == "\(vc.dynamicType)" {
self.popToViewController(element, animated: true)
break
}
}
}
}
Let's say I have a FOHomeVC class (who is a UIViewController) instantiated in the navigation stack.
So I would do this in my code:
self.navigationController?.backToViewController(FOHomeVC.self)

I have added an extension to UINavigationController which helps you to find if that controller exist in navigation stack. If yes then it will be popped to that controller or else you pass new controller to push with pushController param.
extension UINavigationController {
func containsViewController(ofKind kind: AnyClass) -> Bool {
return self.viewControllers.contains(where: { $0.isKind(of: kind) })
}
func popPushToVC(ofKind kind: AnyClass, pushController: UIViewController) {
if containsViewController(ofKind: kind) {
for controller in self.viewControllers {
if controller.isKind(of: kind) {
popToViewController(controller, animated: true)
break
}
}
} else {
pushViewController(pushController, animated: true)
}
}
}

Swift 4 / Swift 5
for controller in self.navigationController!.viewControllers as Array {
if controller.isKind(of: HomeViewController.self) {
self.navigationController!.popToViewController(controller, animated: true)
break
}
}

I prefer a "real generic" and more functional approach.
So I came up with following UINavigationController extension functions. You can also use the first function, for anything else, where you just need to access a specific VC in the navigation stack.
Extensions
extension UINavigationController {
func getViewController<T: UIViewController>(of type: T.Type) -> UIViewController? {
return self.viewControllers.first(where: { $0 is T })
}
func popToViewController<T: UIViewController>(of type: T.Type, animated: Bool) {
guard let viewController = self.getViewController(of: type) else { return }
self.popToViewController(viewController, animated: animated)
}
}
Usage
self.navigationController?.popToViewController(of: YourViewController.self, animated: true)
This should work at least in Swift 4 and 5.

Find your view controller from navigation stack and pop to that view controller if it exists
for vc in self.navigationController!.viewControllers {
if let myViewCont = vc as? VCName
{
self.navigationController?.popToViewController(myViewCont, animated: true)
}
}

swift5
let controllers : Array = self.navigationController!.viewControllers
self.navigationController!.popToViewController(controllers[1], animated: true)

Swift 5 Answer of #PabloR is Here :
extension UINavigationController {
func backToViewController(vc: Any) {
// iterate to find the type of vc
for element in viewControllers as Array {
if "\(type(of: element)).Type" == "\(type(of: vc))" {
self.popToViewController(element, animated: true)
break
}
}
}
}
Usage :
self.navigationController?.backToViewController(vc: TaskListViewController.self)

In latest swift
#IBAction func popToConversationsVC(_ sender: UIButton) {
if (self.navigationController != nil) {
for vc in self.navigationController!.viewControllers {
if vc is ConversationsVC {
self.navigationController?.popToViewController(vc, animated: false)
}
}
}
}

For Swift 4.0 and above Using Filter
guard let VC = self.navigationController?.viewControllers.filter({$0.isKind(of: YourViewController.self)}).first else {return}
self.navigationController?.popToViewController(VC, animated: true)

Please use this below code for Swift 3.0:
let viewControllers: [UIViewController] = self.navigationController!.viewControllers as [UIViewController];
for aViewController:UIViewController in viewControllers {
if aViewController.isKind(of: YourViewController.self) {
_ = self.navigationController?.popToViewController(aViewController, animated: true)
}
}

I needed to use this, because in some cases app crashes:
if let navVC = self.navigationController {
let views = navVC.viewControllers as Array
for controller in views {
if controller.isKind(of: YourVC.self) {
navVC.popToViewController(controller, animated: true)
return
}
}
}

This solution worked for me :)
extension UINavigationController {
func backToViewController(_ viewController: AnyClass, animated: Bool) {
guard let viewController = self.viewControllers.first(where: {$0.isKind(of: viewController)}) else { return }
self.popToViewController(viewController, animated: animated)
}
}

I adapt from all answer above. It look like Yakup Ad answer, because it's very short way.
I force type by using generic for argument, that make sure you must pass only UIViewController to this func.
I search viewController that already in stack by using .first this make me got only one VC then stop the loop.
I also return passing VC back if you need to customize somethings.
Let's enjoy.
extension UINavigationController {
func popToViewController<T: UIViewController>(_ viewController: T.Type, animated: Bool) -> T? {
guard let viewController = self.viewControllers.first(where: {$0 is T}) else { return nil }
self.popToViewController(viewController, animated: animated)
return viewController as? T
}
}
Usage
let poppedVC = self.navigationController?.popToViewController(HomeViewController.self, animated: true)

extension UINavigationController {
func popBack(to vc: AnyClass, animated: Bool = true) {
guard let elementFound = (viewControllers.filter { $0.isKind(of: vc) }).first else {
fatalError("cannot pop back to \(vc) as it is not in the view hierarchy")
}
self.popToViewController(elementFound, animated: animated)
}
}

simple and best solution without force unwrapped is
if let vc = navigationController.viewControllers.filter({$0 is YourViewController}).first as? YourViewController {
self.navigationController.popToViewController(vc, animated: true)
}

Related

How to test if view controller is dismissed or popped

i want to write an unit test for my function, here is code:
func smartBack(animated: Bool = true) {
if isModal() {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: animated)
}
}
This method automatically chooses dismiss or pop. So, how i can check if viewcontroller popped or dismissed after this function? Thank you for help
You can check the view controller's isBeingDismissed property in either its viewWillAppear or viewDidAppear function.
See https://developer.apple.com/documentation/uikit/uiviewcontroller/2097562-isbeingdismissed.
func smartBack(animated: Bool = true) will be:
func smartBack(animated: Bool = true) {
if self.navigationController?.viewControllers.first == self {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: true)
}
}
You can use property self.isBeingPresented, will return true is view controller presented otherwise false if pushed.
You could check the viewControllers stack and see if your viewController is included or not, using:
self.navigationController.viewControllers
This will return a [UIViewController] contained in the navigationController stack.
Personally I would use Mocks to track when certain methods are called.
You can do this like so:
class MockNavigationController: UINavigationController {
var _popCalled: Bool = false
override func popViewController(animated: Bool) -> UIViewController? {
_popCalled = true
return self.viewControllers.first
}
}
Then anytime your code calls popViewController, the _popCalled value would be updated but it wouldn't actually pop anything. So you can assert the _popCalled value to make sure that the expected call happened.
This makes it easy to test that an expected thing happened and also prevents you running actual code in your tests. This method could easily be a service call, or db update, setting a flag etc, so can be much safer.
They can be difficult to understand at first though. I would suggest reading up on them before heavy use.
A full example in a playground:
import UIKit
import PlaygroundSupport
import MapKit
class ClassUnderTest: UIViewController {
var isModal: Bool = false
func smartBack(animated: Bool = true) {
if isModal {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: animated)
}
}
}
class MockNavigationController: UINavigationController {
var _popCalled: Bool = false
override func popViewController(animated: Bool) -> UIViewController? {
_popCalled = true
return self.viewControllers.first
}
}
class MockClassUnderTest: ClassUnderTest {
var _mockNavigationController = MockNavigationController()
override var navigationController: UINavigationController? {
return _mockNavigationController
}
var _dismissCalled: Bool = false
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
_dismissCalled = true
}
}
var subject = MockClassUnderTest()
subject.isModal = true
subject.smartBack();
var navigation = subject.navigationController as! MockNavigationController
print(subject._dismissCalled)
print(navigation._popCalled)
OUTPUT:
true
false
subject = MockClassUnderTest();
subject.isModal = false
subject.smartBack();
navigation = subject.navigationController as! MockNavigationController
print(subject._dismissCalled)
print(navigation._popCalled)
OUTPUT:
false
true
In this example, you are overriding the dismiss and pop methods which would be called in either case. In your unit test you would just assert the stubbed values (_popCalled) are true or false for your expectations.
I solved in this way. I have needed to test a simple method that contains this: dismiss(animated: true, completion: nil) and I made a temporal mock that simulates a viewController that do a push to my MainController which it is where I apply the dismissView.
func testValidatesTheDismissOfViewController() {
// Given
let mockViewController: UIViewController = UIViewController()
let navigationController = UINavigationController(rootViewController: mockViewController)
// Create instance of my controller that is going to dismiss.
let sut: HomeWireFrame = HomeWireFrame().instanceController()
navigationController.presentFullScreen(sut, animated: true)
// When
sut.loadViewIfNeeded()
sut.closeView()
// Create an expectation...
let expectation = XCTestExpectation(description: "Dismiss modal view: HomeViewController")
// ...then fulfill it asynchronously
DispatchQueue.main.async { expectation.fulfill() }
wait(for: [expectation], timeout: 1)
// Then - if its different of my home controller
XCTAssertTrue(!(navigationController.topViewController is HomeViewController))
}
I hope can help, I´m here to any doubt.
It is worked for me:
func smartBack(animated: Bool = true) {
if self.navigationController == nil {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: true)
}
}

Not possible to transfer the data back to the ViewController

I am having issues trying to pass the data back to the ViewController (from BarCodeScannerViewController to TableViewController)
SecondVC (BarCodeScannerViewController.swift):
#objc func SendDataBack(_ button:UIBarButtonItem!) {
if let presenter = self.presentingViewController as? TableViewController {
presenter.BarCode = "Test"
}
self.dismiss(animated: true, completion: nil)
}
FirstVC (TableViewController.swift):
// The result is (BarCode - )
var BarCode: String = ""
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("BarCode - \(BarCode)")
}
Each time ViewWillAppear is running the value is not set, what could be causing this issue?
You should use the delegate pattern. I doubt in your code above that self.presentingViewController is actually set.
An example of using the delegate pattern for this:
// BarCodeScannerViewController.swift
protocol BarcodeScanningDelegate {
func didScan(barcode: String)
}
class BarCodeScannerViewController: UIViewController {
delegate: BarcodeScanningDelegate?
#objc func SendDataBack(_ button:UIBarButtonItem!) {
delegate?.didScan(barcode: "Test")
}
}
// TableViewController
#IBAction func scanBarcode() {
let vc = BarCodeScannerViewController()
vc.delegate = self
self.present(vc, animated: true)
}
extension TableViewController: BarcodeScanningDelegate {
func didScan(barcode: String) {
print("[DEBUG] - Barcode scanned: \(barcode)")
}
}

iOS Swift3 check nil value for ViewController Object

let viewControllers: [UIViewController] = self.navigationController!.viewControllers
for VC in viewControllers {
if (VC.isKind(of: HomeViewController.self)) {
bScreen = true
self.navigationController?.popToViewController(VC, animated: true)
}
}
if bScreen == false {
let homeVC = HomeViewController()
self.navigationController?.pushViewController(homeVC, animated: false)
}
I loop through navigation controller array to move to HomeViewController.above code is working fine.some times i am getting crash as “fatal error: unexpectedly found nil while unwrapping an Optional value”.I know the cause for this crash.Please help me how to check nil value for view controller object.any help will be appreciated.thanks in advance
-- Swift 3 --
for vc in (self.navigationController?.viewControllers)! {
if vc is HomeViewController {
_ = self.navigationController?.popToViewController(vc, animated: true)
}
}
Use this code. this is helpful for you.
let viewControllers: [UIViewController] = self.navigationController!.viewControllers
for VC in viewControllers {
if (VC.isKind(of: HomeViewController.self)) {
bScreen = true
self.navigationController?.popToViewController(VC, animated: true)
break;
}
}
if bScreen == false
{
let homeVC = HomeViewController()
self.navigationController?.pushViewController(homeVC, animated: false)
}
let getCurrentVCIndex = self.navigationController?.viewControllers.indexOf({ (viewController) -> Bool in
if let _ = viewController as? HomeViewController {
return true
}
return false
})
if getCurrentVCIndex
{
let HomeVC = self.navigationController?.viewControllers[getCurrentVCIndex!] as! HomeViewController
self.navigationController?.popToViewController(HomeVC, animated: true)
}
else
{
// use push
}
or use like
if let HomeVC = self.navigationController?.viewControllers.filter({$0 is HomeViewController}).first
{
self.navigationController?.popToViewController(HomeVC!, animated: true)
}else
{
// use push
}
Never use directly ! until you are damn sure that it will not be nil. Replace your code as below. You can use if let or guard let to unwrap optionals.
if let viewControllers: [UIViewController] = self.navigationController?.viewControllers {
for VC in viewControllers {
if (VC.isKind(of: ViewController.self)) {
bScreen = true
self.navigationController?.popToViewController(VC, animated: true)
}
}
if bScreen == false
{
let homeVC = ViewController()
self.navigationController?.pushViewController(homeVC, animated: false)
}
}
else {
// IF VC is nil
}
This is better to use to if let / guard for an optional value to avoid crashing.
if let viewControllers: [UIViewController] = self.navigationController.viewControllers{
for VC in viewControllers {
if (VC.isKind(of: HomeViewController.self)) {
bScreen = true
self.navigationController?.popToViewController(VC, animated: true)
}
}
if bScreen == false
{
let homeVC = HomeViewController()
self.navigationController?.pushViewController(homeVC, animated: false)
}
}
Based on your code, In the loop, if the navigation stack contains the respective view controller will be popped to the respective page. But the thing is if the same view controller is present two times, will lead to execute the loop for the same time. This may cause crash. So Add a break after the poptoviewcontroller will avoid this issue. Please check the below code, will help you.
if (VC.isKind(of: HomeViewController.self)) {
bScreen = true
self.navigationController?.popToViewController(VC, animated: true)
break
}

Show two ViewController from AppDelegate

When APP is Launching - start SigninView - it's Okey. Next if success - I need showTripController(). Function work but nothing show? What's a problem?
func showSigninView() {
let controller = self.window?.rootViewController!.storyboard?.instantiateViewControllerWithIdentifier("DRVAuthorizationViewController")
self.window?.rootViewController!.presentViewController(controller!, animated: true, completion: nil)
}
func showTripController() {
let cv = self.window?.rootViewController!.storyboard?.instantiateViewControllerWithIdentifier("DRVTripTableViewController")
let nc = UINavigationController()
self.window?.rootViewController!.presentViewController(nc, animated:true, completion: nil)
nc.pushViewController(cv!, animated: true);
}
First of all you must add this before you use window :
self.window.makeKeyAndVisible()
Another thing to keep in mind is:
Sometimes keyWindow may have been replaced by window with nil rootViewController (showing UIAlertViews, UIActionSheets on iPhone, etc), in that case you should use UIView's window property.
So, instead of using rootViewController, use the top one presented by it:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
if let topController = UIApplication.topViewController() {
topController.presentViewController(vc, animated: true, completion: nil)
}
Replace last 3 lines of showTripController as below:
let nc = UINavigationController(rootViewController: cv));
self.window!.rootViewController = nc

Warning: Attempt to present * on * whose view is not in the window hierarchy - swift

I'm trying to present a ViewController if there is any saved data in the data model. But I get the following error:
Warning: Attempt to present * on *whose view is not in the window hierarchy"
Relevant code:
override func viewDidLoad() {
super.viewDidLoad()
loginButton.backgroundColor = UIColor.orangeColor()
var request = NSFetchRequest(entityName: "UserData")
request.returnsObjectsAsFaults = false
var appDel:AppDelegate = (UIApplication.sharedApplication().delegate as AppDelegate)
var context:NSManagedObjectContext = appDel.managedObjectContext!
var results:NSArray = context.executeFetchRequest(request, error: nil)!
if(results.count <= 0){
print("Inga resultat")
} else {
print("SWITCH VIEW PLOX")
let internVC = self.storyboard?.instantiateViewControllerWithIdentifier("internVC") as internViewController
self.presentViewController(internVC, animated: true, completion: nil)
}
}
I've tried different solutions found using Google without success.
At this point in your code the view controller's view has only been created but not added to any view hierarchy. If you want to present from that view controller as soon as possible you should do it in viewDidAppear to be safest.
In objective c:
This solved my problem when presenting viewcontroller on top of mpmovieplayer
- (UIViewController*) topMostController
{
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
Swift 3
I had this keep coming up as a newbie and found that present loads modal views that can be dismissed but switching to root controller is best if you don't need to show a modal.
I was using this
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard?.instantiateViewController(withIdentifier: "MainAppStoryboard") as! TabbarController
present(vc, animated: false, completion: nil)
Using this instead with my tabController:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let view = storyboard.instantiateViewController(withIdentifier: "MainAppStoryboard") as UIViewController
let appDelegate = UIApplication.shared.delegate as! AppDelegate
//show window
appDelegate.window?.rootViewController = view
Just adjust to a view controller if you need to switch between multiple storyboard screens.
Swift 3.
Call this function to get the topmost view controller, then have that view controller present.
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
Usage:
let topVC = topMostController()
let vcToPresent = self.storyboard!.instantiateViewController(withIdentifier: "YourVCStoryboardID") as! YourViewController
topVC.present(vcToPresent, animated: true, completion: nil)
for SWIFT
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.sharedApplication().keyWindow!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
You just need to perform a selector with a delay - (0 seconds works).
override func viewDidLoad() {
super.viewDidLoad()
perform(#selector(presentExampleController), with: nil, afterDelay: 0)
}
#objc private func presentExampleController() {
let exampleStoryboard = UIStoryboard(named: "example", bundle: nil)
let exampleVC = storyboard.instantiateViewController(withIdentifier: "ExampleVC") as! ExampleVC
present(exampleVC, animated: true)
}
Swift 4
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
Use of main thread to present and dismiss view controller worked for me.
DispatchQueue.main.async { self.present(viewController, animated: true, completion: nil) }
I was getting this error while was presenting controller after the user opens the deeplink.
I know this isn't the best solution, but if you are in short time frame here is a quick fix - just wrap your code in asyncAfter:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7, execute: { [weak self] in
navigationController.present(signInCoordinator.baseController, animated: animated, completion: completion)
})
It will give time for your presenting controller to call viewDidAppear.
For swift 3.0 and above
public static func getTopViewController() -> UIViewController?{
if var topController = UIApplication.shared.keyWindow?.rootViewController
{
while (topController.presentedViewController != nil)
{
topController = topController.presentedViewController!
}
return topController
}
return nil}
let storyboard = UIStoryboard(name: "test", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "teststoryboard") as UIViewController
UIApplication.shared.keyWindow?.rootViewController?.present(vc, animated: true, completion: nil)
This seemed to work to make sure it's the top most view.
I was getting an error
Warning: Attempt to present myapp.testController: 0x7fdd01703990 on myapp.testController: 0x7fdd01703690 whose view is not in the window hierarchy!
Hope this helps others with swift 3
I have tried so many approches! the only useful thing is:
if var topController = UIApplication.shared.keyWindow?.rootViewController
{
while (topController.presentedViewController != nil)
{
topController = topController.presentedViewController!
}
}
All implementation for topViewController here are not fully supporting cases when you have UINavigationController or UITabBarController, for those two you need a bit different handling:
For UITabBarController and UINavigationController you need a different implementation.
Here is code I'm using to get topMostViewController:
protocol TopUIViewController {
func topUIViewController() -> UIViewController?
}
extension UIWindow : TopUIViewController {
func topUIViewController() -> UIViewController? {
if let rootViewController = self.rootViewController {
return self.recursiveTopUIViewController(from: rootViewController)
}
return nil
}
private func recursiveTopUIViewController(from: UIViewController?) -> UIViewController? {
if let topVC = from?.topUIViewController() { return recursiveTopUIViewController(from: topVC) ?? from }
return from
}
}
extension UIViewController : TopUIViewController {
#objc open func topUIViewController() -> UIViewController? {
return self.presentedViewController
}
}
extension UINavigationController {
override open func topUIViewController() -> UIViewController? {
return self.visibleViewController
}
}
extension UITabBarController {
override open func topUIViewController() -> UIViewController? {
return self.selectedViewController ?? presentedViewController
}
}
The previous answers relate to the situation where the view controller that should present a view 1) has not been added yet to the view hierarchy, or 2) is not the top view controller.
Another possibility is that an alert should be presented while another alert is already presented, and not yet dismissed.
Swift Method, and supply a demo.
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.sharedApplication().keyWindow!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
func demo() {
let vc = ViewController()
let nav = UINavigationController.init(rootViewController: vc)
topMostController().present(nav, animated: true, completion: nil)
}
Swift 5.1:
let storyboard = UIStoryboard.init(name: "Main", bundle: Bundle.main)
let mainViewController = storyboard.instantiateViewController(withIdentifier: "ID")
let appDeleg = UIApplication.shared.delegate as! AppDelegate
let root = appDeleg.window?.rootViewController as! UINavigationController
root.pushViewController(mainViewController, animated: true)
Rather than finding top view controller, one can use
viewController.modalPresentationStyle = UIModalPresentationStyle.currentContext
Where viewController is the controller which you want to present
This is useful when there are different kinds of views in hierarchy like TabBar, NavBar, though others seems to be correct but more sort of hackish
The other presentation style can be found on apple doc

Resources