Swift generic function to push any view controller

Viewed 2017

I am attempting to write a func that 1) instantiates a subclass of UIViewController and 2) pushes into the navigation controller of the caller UIViewController.

So far, I have this:

func pushAnyViewController<T>(viewController:T, storyboardName:String) {
    // Instantiate the view controller of type T
    guard let nextViewController = UIStoryboard(name: storyboardName, bundle: nil).instantiateViewController(withIdentifier: String(describing: T.self)) as? T else {
        return
    }      
    
    viewController.navigationController.pushViewController(nextViewController, animated: true)
}

This produces error

Value of type 'T' has no member 'navigationController'

I am not sure if somehow I should say that T will always be a subclass of UIViewController. If that is the case, it's not clear where I do that. For this, I thought about:

func pushAnyViewController<T>(viewController:T & UIViewController, storyboardName:String)

but that produces errors:

Generic parameter 'T' is not used in function signature

Non-protocol, non-class type 'T' cannot be used within a protocol-constrained type

3 Answers

You need to identify that T is a vc with <T:UIViewController>

func pushAnyViewController<T:UIViewController>(viewController:T, storyboardName:String) {
    guard let nextViewController = UIStoryboard(name: storyboardName, bundle: nil).instantiateViewController(withIdentifier: String(describing: T.self)) as? T else { return } 
    viewController.navigationController?.pushViewController(nextViewController, animated: true)
}

The top answer didn't suit me, I needed a more generic solution. First, I needed the option to pass data, second, I wanted to have a generic function that can push or present VC, and last, I wanted that my generic function can be called from anywhere, not just from the UIViewController, that's why an extension of UIViewController didn't suit me.

I decided to create a struct with simple init, and two public methods so that I can create the copy anywhere and call those methods.

struct Navigator {
    
    // MARK: - DisplayVCType enum

    enum DisplayVCType {
        case push
        case present
    }
    
    // MARK: - Properties
    
    private var viewController: UIViewController
    static private let mainSBName = "Main"

    // MARK: - Init

    init(vc: UIViewController) {
        self.viewController = vc
    }
    
    // MARK: - Public Methods
    
    public func instantiateVC<T>(withDestinationViewControllerType vcType: T.Type,
                                            andStoryboardName sbName: String = mainSBName) -> T? where T: UIViewController {
        let storyBoard: UIStoryboard = UIStoryboard(name: sbName, bundle: nil)
        let destinationVC = storyBoard.instantiateViewController(withIdentifier: String(describing: vcType.self))

        return destinationVC as? T
    }
    
    public func goTo(viewController destinationVC: UIViewController,
              withDisplayVCType type: DisplayVCType = .present,
              andModalPresentationStyle style: UIModalPresentationStyle = .popover) {
        switch type {
        case .push:
            viewController.navigationController?.pushViewController(destinationVC, animated: true)
        case .present:
            destinationVC.modalPresentationStyle = style
            viewController.present(destinationVC, animated: true, completion: nil)
        }
    }
}

and example call in some VC, with passing string after push:

class SomeVC: UIViewController {

    var navigator: Navigator?

    override func viewDidLoad() {
        super.viewDidLoad()
        navigator = Navigator(vc: self)
    }

    func pushVC() {
        guard let vc = navigator?.instantiateVC(withDestinationViewControllerType: VC1.self) else { return }
        vc.someString = "SOME STRING TO BE PASSED"
        navigator?.goTo(viewController: vc, withDisplayVCType: .push)
    }
    
    func presentVC() {
        guard let vc = navigator?.instantiateVC(withDestinationViewControllerType: TableViewController.self) else { return }
        navigator?.goTo(viewController: vc, withDisplayVCType: .present)
    }
}

With Help of some keyword.

enum Storyboard : String {
    case Main
}

func viewController(_ viewController: UIViewController.Type) -> some UIViewController {
        return UIStoryboard(name: self.rawValue, bundle: nil).instantiateViewController(withIdentifier: String(describing: viewController.self))
    }
Related