Swift generic function to push any view controller - ios

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

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

How to pass UIVIewController name as a parameter to particular function using swift?

In my scenario, I need to pass UIVIewController name and some more string values to particular function. I tried below code but not getting result.
Passing Parameters To Particular Function
self.accountoptionscall(vcName: UIViewController(), vcIdentifier: "profileviewcontroller", popUpVC: ProfileViewController.self)
func accountoptionscall<T: UIViewController>(vcName: UIViewController,vcIdentifier: String, popUpVC: T.self) {
let viewcontrollers = self.storyboard!.instantiateViewController(withIdentifier: vcIdentifier) as! vcName
let navController = UINavigationController(rootViewController: viewcontrollers)
self.present(navController, animated:true, completion: nil)
}
I use it in my app, I think it can help you.
extension UIViewController {
/// Load UIViewController type from UIStoryboard
class func loadFromStoryboard<T: UIViewController>() -> T {
let name = String(describing: T.self)
let storybord = UIStoryboard(name: name, bundle: nil)
if let viewController = storybord.instantiateInitialViewController() as? T {
return viewController
} else {
fatalError("Error: No initial view controller in \(name) storyboard!")
}
}
}
Here how you can use it:
func loadVC<T: UIViewController>(controller: T) {
let vc: T = T.loadFromStoryboard()
let navigationVC = UINavigationController(rootViewController: vc)
self.present(navController, animated:true, completion: nil)
// or if use in appDelegate you can do it: window?.rootViewController = navigationVC
}

nil Delegate between two ViewController with two different Bundle (swift)

nil Delegate between two ViewController with two different Bundle using swift 4 (commented in second code)
here is my code :
First ViewController :
class FirstVC : UIViewController, MerchantResultObserver{
var secVC = SecondVC()
override func viewDidLoad() {
secVC.delegate = self
let storyboard = UIStoryboard(name: “SecondVC”, bundle: Bundle(identifier: “SecondBundle”))
let controller = storyboard.instantiateInitialViewController()
self.present(controller!, animated: true, completion: nil)
secVC.initSecondVC(data)
}
func Error(data: String) {
print("-------------Error Returned------------- \(data)")
}
func Response(data: String) {
print("-------------Response Returned------------- \(data)")
}
}
Second ViewController :
public class SecondVC: UIViewController {
public weak var delegate: MerchantResultObserver!
public func initSecondVC(_ data : String){
print(data)
}
#IBAction func sendRequest(_ sender: UIButton) {
delegate?.Response(data: “dataReturnedSuccessfully”) // delegate is nil //
dismiss(animated: true, completion: nil) // returned to FirstVC without returning “dataReturnedSuccessfully” //
}
}
public protocol MerchantResultObserver: class{
func Response(data : String)
func Error(data : String)
}
Any help would be appreciated
var secVC = SecondVC()
and
let storyboard = UIStoryboard(name: “SecondVC”, bundle: Bundle(identifier: “SecondBundle”))
let controller = storyboard.instantiateInitialViewController() as? SecondVC
These both are different instances.
You can assign a delegate to the controller, like
controller.delegate = self
It will call the implemented delegate methods in First View Controller.
Full Code.
let storyboard = UIStoryboard(name: “SecondVC”, bundle: Bundle(identifier: “SecondBundle”))
if let controller = storyboard.instantiateInitialViewController() as? SecondVC {
//Assign Delegate
controller.delegate = self
//It's not init, but an assignment only, as per your code.
controller.initSecondVC(data)
self.present(controller, animated: true, completion: nil)
}
One more thing, Don't present View in ViewDidLoad. You can put a code in some button or in a delay method.

Helper method for pushing UIViewController

Is there a way to instantiate - set - push a view controller using a helper method like this but in the same time avoiding downcasting?
func pushController(id: String, setup: (_ vc: UIViewController) -> ()) {
if let vc = storyboard?.instantiateViewController(withIdentifier: id) {
setup(vc)
navigationController?.pushViewController(vc, animated: true)
}
}
// usage
pushController(id: "Cars") { vc in
(vc as! CarsVC).brand = "BMW"
}
// ...want to avoid downcasting
vc.brand = "BMW"
The most elegant solution I could think of is using generics, like this (playground)-example:
import UIKit
extension UIViewController {
func pushController<T:UIViewController> (id: String, setup: (_ vc: T) -> ()) {
if let vc = self.storyboard?.instantiateViewController(withIdentifier: id) as? T {
setup(vc)
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
class ViewControllerA:UIViewController {}
class ViewControllerB:UIViewController {
var bValue:Int = 0
}
let vcA = ViewControllerA();
vcA.pushController(id: "B") {
(vc:ViewControllerB) in
vc.title = "view controller b"
vc.bValue = 42;
}
I would have preferred calling pushController with an explicit generic type, but unfortunatley this is not supported by Swift 3:
vcA.pushController<ViewControllerB>(id: "B") { // Error: cannot explicitly specialize a generic function
vc in
vc.title = "view controller b"
vc.bValue = 42;
}
I don't think you can avoid downcasting, but you can make it less painful:
func pushController<VC: UIViewController>(id: String, setup: (_ vc: VC) -> ()) {
if let vc = storyboard?.instantiateViewController(withIdentifier: id) as? VC {
setup(vc)
navigationController?.pushViewController(vc, animated: true)
}
}
// usage
pushController(id: "Cars") { (vc: CarsVC) in
vc.brand = "BMW"
}
Not tested, so there might be minor issues.
EDIT: I should note that this fails silently when the wrong type is used with an ID. You may want to add an else after the if to handle this.

How to push and present to UIViewController programmatically without segue in iOS Swift 3

I am using this code for push SHOW and MODALLY programmatically in iOS Objective C.
And now want to know about Swift 3.
NewsDetailsViewController *vc = instantiateViewControllerWithIdentifier:#"NewsDetailsVCID"];
vc.newsObj = newsObj;
//--this(SHOW)
[self.navigationController pushViewController:vc animated:YES];
//-- or this(MODAL)
[self presentViewController:vc animated:YES completion:nil];
Push
do like
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("NewsDetailsVCID") as NewsDetailsViewController
vc.newsObj = newsObj
navigationController?.pushViewController(vc,
animated: true)
or safer
if let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "NewsDetailsVCID") as? NewsDetailsViewController {
viewController.newsObj = newsObj
if let navigator = navigationController {
navigator.pushViewController(viewController, animated: true)
}
}
present
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = self.storyboard?.instantiateViewControllerWithIdentifier("NewsDetailsVCID") as! NewsDetailsViewController
vc.newsObj = newsObj
present(vc!, animated: true, completion: nil)
or safer
if let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "NewsDetailsVCID") as? NewsDetailsViewController
{
vc.newsObj = newsObj
present(vc, animated: true, completion: nil)
}
With an elegant way.
Create an Navigatable protocol:
protocol Navigatable {
/// Storyboard name where this view controller exists.
static var storyboardName: String { get }
/// Storyboard Id of this view controller.
static var storyboardId: String { get }
/// Returns a new instance created from Storyboard identifiers.
static func instantiateFromStoryboard() -> Self
}
Create a default instantiate controller implementation:
/**
Extension of Navigatable protocol with default implementations.
*/
extension Navigatable {
static func instantiateFromStoryboard() -> Self {
let storyboard = UIStoryboard(name: self.storyboardName, bundle: nil)
guard
let viewController = storyboard
.instantiateViewController(withIdentifier: self.storyboardId) as? Self else {
fatalError("Cannot instantiate the controller.")
}
return viewController
}
}
Extends the UIViewController to push a view controller:
extension UIViewController {
/**
Pushes a view controller of the provided type.
- Parameter viewControllerType: Type of view controller to push.
- Parameter completion: Function to be executed on completion.
Contains the view controller that was pushed when successful and nil otherwise.
*/
func pushViewControllerOfType<T: Navigatable>(viewControllerType: T.Type, completion: (T) -> Void) {
let viewController = T.instantiateFromStoryboard()
if let vc = viewController as? UIViewController {
self.pushViewController(vc, animated: true)
}
completion(viewController)
}
/**
Pushes a view controller of the provided type.
- Parameter viewControllerType: Type of view controller to push.
*/
func pushViewControllerOfType<T: Navigatable>(viewControllerType: T.Type) {
self.pushViewControllerOfType(viewControllerType: viewControllerType) { _ in }
}
}
Then you can use the Navigatable protocol for a specific view controller.
class MySuperViewController {
override func viewDidLoad() {
...
}
// ...
}
extension MySuperViewController: Navigatable {
static var storyboardName: String {
return "Main"
}
static var storyboardId: String {
return "MySuperViewControllerId" // From your story board name Main
}
}
// Instantiate your controller
let vc = MySuperViewController.instantiateFromStoryboard()
// Or
//
// Push your view controller
// testViewController.swift
self.pushViewControllerOfType(viewControllerType: MySuperViewController)
//Create object of view controller
let obj = self.storyboard?.instantiateViewController(withIdentifier: "ViewControllerIdentifier”) as! ViewControllerName
//Push Controller
self.navigationController?.pushViewController(obj, animated: true)
//Present Controller
self.navigationController?.present(obj, animated: true, completion: nil)

Pushing ViewController from string in Swift

I have a class 'MyViewController.swift' and a string "MyViewController". How can I create an instance of that viewController from the string and push it in the navigation controller?
I've checked this answer (How to create an object depending on a String in Swift?) but it doesn't seem to do the trick.
Assuming you are working with storyboard, you could extend UIStoryboard like:
class func mainStoryboard() -> UIStoryboard {
return UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
}
class func myViewController(s: String) -> UIViewController? {
return mainStoryboard().instantiateViewControllerWithIdentifier(s) as? UIViewController
}
and then you can use it like
myVC = UIStoryboard.myViewController("controller")
myVC.view.frame = view.frame
view.addSubview(myVC.view)
addChildViewController(myVC)
myVC.didMoveToParentViewController(self)
or
let vc = getVC("controller")
vc!.modalTransitionStyle = UIModalTransitionStyle.CrossDissolve
self.presentViewController(vc!, animated: true, completion: nil)
Update:
If you are not using storyboards, you can add a something like this to your controller:
func getVC(s: String) -> UIViewController {
switch s {
case "myVc":
return MyVC() as! UIViewController
default:
// handle default case
}
}

Resources