Completion handler for UINavigationController "pushViewController:animated"? - ios

I'm about creating an app using a UINavigationController to present the next view controllers.
With iOS5 there´s a new method to presenting UIViewControllers:
presentViewController:animated:completion:
Now I ask me why isn´t there a completion handler for UINavigationController?
There are just
pushViewController:animated:
Is it possible to create my own completion handler like the new presentViewController:animated:completion: ?

See par's answer for another and more up to date solution
UINavigationController animations are run with CoreAnimation, so it would make sense to encapsulate the code within CATransaction and thus set a completion block.
Swift:
For swift I suggest creating an extension as such
extension UINavigationController {
public func pushViewController(viewController: UIViewController,
animated: Bool,
completion: #escaping (() -> Void)?) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
pushViewController(viewController, animated: animated)
CATransaction.commit()
}
}
Usage:
navigationController?.pushViewController(vc, animated: true) {
// Animation done
}
Objective-C
Header:
#import <UIKit/UIKit.h>
#interface UINavigationController (CompletionHandler)
- (void)completionhandler_pushViewController:(UIViewController *)viewController
animated:(BOOL)animated
completion:(void (^)(void))completion;
#end
Implementation:
#import "UINavigationController+CompletionHandler.h"
#import <QuartzCore/QuartzCore.h>
#implementation UINavigationController (CompletionHandler)
- (void)completionhandler_pushViewController:(UIViewController *)viewController
animated:(BOOL)animated
completion:(void (^)(void))completion
{
[CATransaction begin];
[CATransaction setCompletionBlock:completion];
[self pushViewController:viewController animated:animated];
[CATransaction commit];
}
#end

iOS 7+ Swift
Swift 4:
// 2018.10.30 par:
// I've updated this answer with an asynchronous dispatch to the main queue
// when we're called without animation. This really should have been in the
// previous solutions I gave but I forgot to add it.
extension UINavigationController {
public func pushViewController(
_ viewController: UIViewController,
animated: Bool,
completion: #escaping () -> Void)
{
pushViewController(viewController, animated: animated)
guard animated, let coordinator = transitionCoordinator else {
DispatchQueue.main.async { completion() }
return
}
coordinator.animate(alongsideTransition: nil) { _ in completion() }
}
func popViewController(
animated: Bool,
completion: #escaping () -> Void)
{
popViewController(animated: animated)
guard animated, let coordinator = transitionCoordinator else {
DispatchQueue.main.async { completion() }
return
}
coordinator.animate(alongsideTransition: nil) { _ in completion() }
}
}
EDIT: I've added a Swift 3 version of my original answer. In this version I've removed the example co-animation shown in the Swift 2 version as it seems to have confused a lot of people.
Swift 3:
import UIKit
// Swift 3 version, no co-animation (alongsideTransition parameter is nil)
extension UINavigationController {
public func pushViewController(
_ viewController: UIViewController,
animated: Bool,
completion: #escaping (Void) -> Void)
{
pushViewController(viewController, animated: animated)
guard animated, let coordinator = transitionCoordinator else {
completion()
return
}
coordinator.animate(alongsideTransition: nil) { _ in completion() }
}
}
Swift 2:
import UIKit
// Swift 2 Version, shows example co-animation (status bar update)
extension UINavigationController {
public func pushViewController(
viewController: UIViewController,
animated: Bool,
completion: Void -> Void)
{
pushViewController(viewController, animated: animated)
guard animated, let coordinator = transitionCoordinator() else {
completion()
return
}
coordinator.animateAlongsideTransition(
// pass nil here or do something animated if you'd like, e.g.:
{ context in
viewController.setNeedsStatusBarAppearanceUpdate()
},
completion: { context in
completion()
}
)
}
}

Based on par's answer (which was the only one that worked with iOS9), but simpler and with a missing else (which could have led to the completion never being called):
extension UINavigationController {
func pushViewController(_ viewController: UIViewController, animated: Bool, completion: #escaping () -> Void) {
pushViewController(viewController, animated: animated)
if animated, let coordinator = transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
func popViewController(animated: Bool, completion: #escaping () -> Void) {
popViewController(animated: animated)
if animated, let coordinator = transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
}

Currently the UINavigationController does not support this. But there's the UINavigationControllerDelegate that you can use.
An easy way to accomplish this is by subclassing UINavigationController and adding a completion block property:
#interface PbNavigationController : UINavigationController <UINavigationControllerDelegate>
#property (nonatomic,copy) dispatch_block_t completionBlock;
#end
#implementation PbNavigationController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.delegate = self;
}
return self;
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
NSLog(#"didShowViewController:%#", viewController);
if (self.completionBlock) {
self.completionBlock();
self.completionBlock = nil;
}
}
#end
Before pushing the new view controller you would have to set the completion block:
UIViewController *vc = ...;
((PbNavigationController *)self.navigationController).completionBlock = ^ {
NSLog(#"COMPLETED");
};
[self.navigationController pushViewController:vc animated:YES];
This new subclass can either be assigned in Interface Builder or be used programmatically like this:
PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];

Here is the Swift 4 version with the Pop.
extension UINavigationController {
public func pushViewController(viewController: UIViewController,
animated: Bool,
completion: (() -> Void)?) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
pushViewController(viewController, animated: animated)
CATransaction.commit()
}
public func popViewController(animated: Bool,
completion: (() -> Void)?) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
popViewController(animated: animated)
CATransaction.commit()
}
}
Just in case someone else needs this.

To expand on #Klaas' answer (and as a result of this question) I've added completion blocks directly to the push method:
#interface PbNavigationController : UINavigationController <UINavigationControllerDelegate>
#property (nonatomic,copy) dispatch_block_t completionBlock;
#property (nonatomic,strong) UIViewController * pushedVC;
#end
#implementation PbNavigationController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.delegate = self;
}
return self;
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
NSLog(#"didShowViewController:%#", viewController);
if (self.completionBlock && self.pushedVC == viewController) {
self.completionBlock();
}
self.completionBlock = nil;
self.pushedVC = nil;
}
-(void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.pushedVC != viewController) {
self.pushedVC = nil;
self.completionBlock = nil;
}
}
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(dispatch_block_t)completion {
self.pushedVC = viewController;
self.completionBlock = completion;
[self pushViewController:viewController animated:animated];
}
#end
To be used as follows:
UIViewController *vc = ...;
[(PbNavigationController *)self.navigationController pushViewController:vc animated:YES completion:^ {
NSLog(#"COMPLETED");
}];

Since iOS 7.0,you can use UIViewControllerTransitionCoordinator to add a push completion block:
UINavigationController *nav = self.navigationController;
[nav pushViewController:vc animated:YES];
id<UIViewControllerTransitionCoordinator> coordinator = vc.transitionCoordinator;
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
NSLog(#"push completed");
}];

Swift 2.0
extension UINavigationController : UINavigationControllerDelegate {
private struct AssociatedKeys {
static var currentCompletioObjectHandle = "currentCompletioObjectHandle"
}
typealias Completion = #convention(block) (UIViewController)->()
var completionBlock:Completion?{
get{
let chBlock = unsafeBitCast(objc_getAssociatedObject(self, &AssociatedKeys.currentCompletioObjectHandle), Completion.self)
return chBlock as Completion
}set{
if let newValue = newValue {
let newValueObj : AnyObject = unsafeBitCast(newValue, AnyObject.self)
objc_setAssociatedObject(self, &AssociatedKeys.currentCompletioObjectHandle, newValueObj, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
func popToViewController(animated: Bool,comp:Completion){
if (self.delegate == nil){
self.delegate = self
}
completionBlock = comp
self.popViewControllerAnimated(true)
}
func pushViewController(viewController: UIViewController, comp:Completion) {
if (self.delegate == nil){
self.delegate = self
}
completionBlock = comp
self.pushViewController(viewController, animated: true)
}
public func navigationController(navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool){
if let comp = completionBlock{
comp(viewController)
completionBlock = nil
self.delegate = nil
}
}
}

It takes a little more pipework to add this behavior and retain the ability to set an external delegate.
Here's a documented implementation that maintains delegate functionality:
LBXCompletingNavigationController

Related

react-native & ios: How to launch View controller from native module manager

I want to launch UIViewController from native module manager.
From react-native: MyNativeModuleManager.viewStreet()
Is this the right way? using DispatchQueue.main.async { to launch UI ViewController.
Most of the times code works but sometimes react-native fails to launch UI View Controller.
I want back button too and hence I am using UINavigationController
//MyNativeModuleManager.m
#import <Foundation/Foundation.h>
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"
#interface RCT_EXTERN_MODULE(MyNativeModuleManager, RCTEventEmitter)
RCT_EXTERN_METHOD(viewStreet)
#end
//MyNativeModuleManager.swift
import Foundation
import UIKit
#objc(MyNativeModuleManager)
class MyNativeModuleManager: RCTEventEmitter {
#objc
override static func requiresMainQueueSetup() -> Bool {
return false
}
override func supportedEvents() -> [String]! {
return []
}
#objc
func viewStreet() -> Void {
DispatchQueue.main.async {
let topController = UIApplication.shared.topMostViewController()
let vc: UIViewController;
vc = ViewStreetUIViewController()
let navController = UINavigationController(rootViewController: vc)
navController.modalPresentationStyle = .fullScreen
_ = vc.view
UIView.transition(with: UIApplication.shared.keyWindow!, duration: 0.65,options: [], animations: {
topController?.present(navController, animated: false, completion: nil)
})
}
}
}
&
//UIApplicationExt.swift
import Foundation
extension UIApplication {
func topMostViewController() -> UIViewController? {
var topViewController: UIViewController? = nil
if #available(iOS 13, *) {
for scene in connectedScenes {
if let windowScene = scene as? UIWindowScene {
for window in windowScene.windows {
if window.isKeyWindow {
topViewController = window.rootViewController
}
}
}
}
} else {
topViewController = keyWindow?.rootViewController
}
while true {
if let presented = topViewController?.presentedViewController {
topViewController = presented
} else if let navController = topViewController as? UINavigationController {
topViewController = navController.topViewController
} else if let tabBarController = topViewController as? UITabBarController {
topViewController = tabBarController.selectedViewController
} else {
// Handle any other third party container in `else if` if required
break
}
}
return topViewController
}
}

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

The next page in my UIPageViewController is only loading partially when the page changes

I have a UIPageViewController PageVC containing 6 view controllers I created in my storyboard. My PageVC class is written in Swift.
class PageVC: UIPageViewController, UIPageViewControllerDelegate {
lazy var VCArr: [UIViewController] = {
return [self.VCInstance(name: "OB1"),
self.VCInstance(name: "OB2"),
self.VCInstance(name: "OB3"),
self.VCInstance(name: "OB4"),
self.VCInstance(name: "OB5"),
self.VCInstance(name: "OB6")]
}()
private func VCInstance(name: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: name)
}
override public func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
if let OB1 = VCArr.first {
setViewControllers([OB1], direction: .forward, animated: true, completion: nil)
}
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?{
guard let viewControllerIndex = VCArr.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return VCArr.last
}
guard VCArr.count > previousIndex else {
return nil
}
return VCArr[previousIndex]
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?{
guard let viewControllerIndex = VCArr.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
guard nextIndex < VCArr.count else {
return VCArr.first
}
guard VCArr.count > nextIndex else {
return nil
}
return VCArr[nextIndex]
}
public func presentationCount(for pageViewController: UIPageViewController) -> Int{
return VCArr.count
}
public func presentationIndex(for pageViewController: UIPageViewController) -> Int{
guard let OB1 = viewControllers?.first,
let OB1Index = VCArr.index(of: OB1) else {
return 0
}
return OB1Index
}
public func nextPageWithIndex(index: Int)
{
let nextVC = VCArr[index]
setViewControllers([nextVC], direction: .forward, animated: true, completion: nil)
}
}
In each of the contained view controllers I have buttons that advance to the next index of PageVC. Most of the contained view controllers have classes written in Objective-C so I called the next page method like this:
-(void)pageNext {
PageVC *vc = (PageVC *) self.parentViewController;
[vc nextPageWithIndexWithIndex:2];
}
The transition above works great, and OB3 loads completely. In OB3 I have an IBOutlet to a button that presents my notification permissions dialog with my next page method called in the completion handler like this.
- (IBAction)enableBtn:(id)sender {
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionSound)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
[self pageNext];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"notification"];
[[NSUserDefaults standardUserDefaults] synchronize];
FIRUser *user = [[FIRAuth auth] currentUser];
NSString *topic = [#"/topics/ios_user_" stringByAppendingString:user.uid];
[[FIRMessaging messaging] subscribeToTopic:topic];
}];
[[UIApplication sharedApplication] registerForRemoteNotifications];
}
The page transitions smoothly when I tap "Allow" in the notification permissions dialog, but some of the elements on OB4 don't load for almost 10 seconds after the transition happens. For example, buttons appear but the text within isn't visible, and neither are UILabels. If I wait about 10 seconds they do appear. I've tried rearranging the pages in the PageVC index, but no matter which page I use the page always appears partially loaded. There are no animations attached to the elements that aren't loading properly. I think it's something to do with the completion handler in the permissions dialog. How can I make the next page in my UIPageViewController load completely upon transition?
Sounds like you are updating the UI from a non-main thread. Try putting the [self pageNext] inside a dispatch to the main queue:
dispatch_async(dispatch_get_main_queue(), ^{ //update UI in main thread.
[self pageNext]
});

Swapping centreViewControllers with FloatingDrawers

I am using a third party pod KGFloatingDrawer which is great because it achieves this:
and is a reimplementation of JVFloatingDrawer. I used their sample code and the sliding drawers are working great!
BUT
When I first run my app I call one centreViewController with no drawers (Login). Then after login I call a new centreViewController with
appDelegate.centerViewController = appDelegate.navigationBarController()
which only works if I restart the app. Am I missing something?
The logout seems fine though
appDelegate.centerViewController = appDelegate.drawerSettingsViewController()
which puzzles me a bit because then I think I'm on the right track?
Am I supposed to only use normal segues and such first and then only call the drawerViewController?
Here is the other code when setting up the floating drawers :
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.rootViewController = drawerViewController
window?.makeKeyAndVisible()
return true
}
private var _drawerViewController: KGDrawerViewController?
var drawerViewController: KGDrawerViewController {
get {
if let viewController = _drawerViewController {
return viewController
}
return prepareDrawerViewController()
}
}
func prepareDrawerViewController() -> KGDrawerViewController {
let drawerViewController = KGDrawerViewController()
drawerViewController.centerViewController = drawerSettingsViewController()
drawerViewController.leftViewController = leftViewController()
drawerViewController.rightViewController = rightViewController()
drawerViewController.backgroundImage = UIImage(named: "sky3")
_drawerViewController = drawerViewController
return drawerViewController
}
private func drawerStoryboard() -> UIStoryboard {
let storyboard = UIStoryboard(name: StoryboardIDs.MainStoryBoardID , bundle: nil)
return storyboard
}
private func viewControllerForStoryboardId(storyboardId: String) -> UIViewController {
let viewController: UIViewController = drawerStoryboard().instantiateViewControllerWithIdentifier(storyboardId)
return viewController
}
func drawerSettingsViewController() -> UIViewController {
let viewController = viewControllerForStoryboardId(StoryboardIDs.LoginViewConSid)
return viewController
}
func sourcePageViewController() -> UIViewController {
let viewController = viewControllerForStoryboardId(StoryboardIDs.SettingsViewConID)
return viewController
}
func navigationBarController() -> UIViewController{
let viewController = viewControllerForStoryboardId(StoryboardIDs.NavConSid)
return viewController
}
private func leftViewController() -> UIViewController {
let viewController = viewControllerForStoryboardId(StoryboardIDs.LeftViewConID)
return viewController
}
private func rightViewController() -> UIViewController {
let viewController = viewControllerForStoryboardId(StoryboardIDs.RightViewConID)
return viewController
}
func toggleLeftDrawer(sender:AnyObject, animated:Bool) {
_drawerViewController?.toggleDrawer(.Left, animated: animated, complete: { (finished) -> Void in
// do nothing
})
}
func toggleRightDrawer(sender:AnyObject, animated:Bool) {
_drawerViewController?.toggleDrawer(.Right, animated: animated, complete: { (finished) -> Void in
// do nothing
})
}
func closeDrawer(sender:AnyObject, animated:Bool){
_drawerViewController?.closeDrawer(.Left, animated: animated, complete: { (finished) -> Void in
})
}
private var _centerViewController: UIViewController?
var centerViewController: UIViewController {
get {
if let viewController = _centerViewController {
return viewController
}
return drawerSettingsViewController()
}
set {
if let drawerViewController = _drawerViewController {
drawerViewController.closeDrawer(drawerViewController.currentlyOpenedSide, animated: true) { finished in }
if drawerViewController.centerViewController != newValue {
drawerViewController.centerViewController = newValue
}
}
_centerViewController = newValue
}
}
Any help/suggestions would be appreciated :D
Just gonna put this here in case anyone has similar problems.
After a week long struggle to find the problem. I eventually found that whenever I changed the centreViewController with
appDelegate.centerViewController = appDelegate.navigationBarController()
OR
appDelegate.centerViewController = appDelegate.logoutController()
that the methods
deinit {
print("deinit called")
notifCentre.removeObserver(self)
}
were not being called in any of the viewControllers.
So I added the line
self.dismissViewControllerAnimated(false, completion: {})
every time that I change the centreViewController.
Apparently Swift normally deinits automagically but when the using the third party methods there is some confusion with the memory handler and we need to step in. Good to know though as it could be a general swift issue as well.

How can I pop specific View Controller in Swift

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

Resources