Helper method for pushing UIViewController - ios

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.

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
}

Swift generic function to push any view controller

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

through parameter determine subclass type , but get error " Use of undeclared type ". How can I do?

I am getting below error at this line if vc is targetVC
"Use of undeclared type 'targetVC'"
func popToTargetController(_ targetVC: UIViewController) -> Bool {
guard let currentNv = tabBar?.selectedViewController as? UINavigationController else{
return false
}
for vc in currentNv.viewControllers {
if vc is targetVC {
currentNv.popToViewController(vc, animated: false)
return true
}
}
return false
}
I have declared class like MovieController inherit form UIViewController, and I want pass MovieController as parameter to this method.
I want to use like this:
class MovieController: UIViewController {
....
....
let _ = someModel.popToTargetController(MovieController)
....
}
I think I see what you are trying to do here.
You are trying to find the targetVC in the navigation stack, so that you can pop all the VCs on top of the targetVC.
When you say if vc is targetVC, that makes sense in English. But what you really mean in terms of Swift, is to check that vc and targetVC are the same type of VC.
To fix this, you need to introduce a generic type:
func popToTargetController<T: UIViewController>(_ targetVCType: T.Type) -> Bool {
guard let currentNv = tabBar?.selectedViewController as? UINavigationController else{
return false
}
for vc in currentNv.viewControllers {
if vc is T {
currentNv.popToViewController(vc, animated: false)
return true
}
}
return false
}
And pass your MovieController like this:
popToTargetController(MovieController.self)
Updating answer from comment:
targetVC is not a type. Just your argument name.
func popToTargetController(_ targetVC: MovieController) -> Bool {
guard let currentNv = tabBar?.selectedViewController as?
UINavigationController else {
return false
}
for vc in currentNv.viewControllers {
if vc is MovieController {
currentNv.popToViewController(vc, animated: false)
return true
}
}
return false
}

Global Popover Function in Swift 1.2 on iOS 8

I have this function that I use all over my app, and it would be nice to create a global function:
class CustomReportVC: UIViewController, UIAdaptivePresentationControllerDelegate, UIPopoverPresentationControllerDelegate {
func showPicker(pickerValues:[String], field:UITextField) -> AnyPickerVC{
//Set up modal
let storyboard = UIStoryboard(name: "Popovers", bundle: nil)
let modal = storyboard.instantiateViewControllerWithIdentifier("AnyPickerModal") as! AnyPickerVC
modal.modalPresentationStyle = UIModalPresentationStyle.Popover
let pc = modal.popoverPresentationController
pc?.permittedArrowDirections = .Down
pc?.sourceView = field
pc?.sourceRect = field.bounds
modal.preferredContentSize = CGSizeMake(300,180)
pc?.delegate = self
//Pass in data
modal.data = pickerValues
//Set the value from within the picker controller
modal.passDataToParent = { (value) in
field.text = value
}
return modal
}
//Required for the popover
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
}
The issue I'm running into comes with pc?.delegate = self. Since CustomReportVC conforms to UIPopoverPresentationControllerDelegate, this works fine.
But once I attempt to create this as a global function outside a class that conforms to this protocol, I get an error:
func globalShowPicker(pickerValues:[String], field:UITextField, controller:UIViewController) -> AnyPickerVC{
//...
pc?.delegate = controller //<-- ( ! ) Type UIViewController does not conform to UIPopoverPresentationControllerDelegate
}
Whether I make controller a UIViewController or AnyObject, it doesn't conform. Is there a way to pass in the protocol conformity to the global function somehow?
Any idea how I can pull this off? Thanks. :)
Make your global function generic to specify that it only works for certain kinds of UIViewControllers. In this example, T can take the value of any UIViewController type which also conforms to the other protocols listed.
func globalShowPicker< T: UIViewController where
T: UIPopoverPresentationControllerDelegate,
T: UIAdaptivePresentationControllerDelegate >
(pickerValues:[String], field:UITextField, controller: T) -> AnyPickerVC
{
//...
pc?.delegate = controller
return blah
}
It does get kinda long, and I haven't figured out the best way to indent all the constraints. But it works.
Try adding a making a new class that inherits from both of them. Like this.
class PopoverController: UIViewController, UIPopoverPresentationControllerDelegate {
}
Then, switch the function to look like this.
func globalShowPicker(pickerValues:[String], field:UITextField, controller: PopoverController) -> AnyPickerVC{
//...
pc?.delegate = controller
}
You can pass the delegate as a parameter in the function like this:
class CustomReportVC: UIViewController, UIAdaptivePresentationControllerDelegate, UIPopoverPresentationControllerDelegate {
class func showPicker(pickerValues:[String], field:UITextField, delegate: UIPopoverPresentationControllerDelegate) -> UIViewController {
//Set up modal
let storyboard = UIStoryboard(name: "Popovers", bundle: nil)
let modal = storyboard.instantiateViewControllerWithIdentifier("AnyPickerModal") as! UIViewController
modal.modalPresentationStyle = UIModalPresentationStyle.Popover
let pc = modal.popoverPresentationController
pc?.permittedArrowDirections = .Down
pc?.sourceView = field
pc?.sourceRect = field.bounds
modal.preferredContentSize = CGSizeMake(300,180)
pc?.delegate = delegate
//Pass in data
modal.data = pickerValues
//Set the value from within the picker controller
modal.passDataToParent = { (value) in
field.text = value
}
return modal
}
//Required for the popover
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
}
You have at the end to have an instance of a view controller that does conform to the protocol, but that way you will have the function global just like you want and pass self pointer in the view controller that does conform to the protocol "UIPopoverPresentationControllerDelegate":
CustomReportVC.showPicker(pickerValues:....., delegate: self)
Something like this?
self.presentViewController(pc, animated: true, completion: nil)
Btw, if you're doing Universal you can not present iPad UIActivityViewController like iPhone. You need to present it in a popover as per the design guidelines suggested by Apple.
or as an example
#IBAction func shareButtonClicked(sender: UIButton)
{
let textToShare = "Text"
if let myWebsite = NSURL(string: "http://www.example.com/")
{
let objectsToShare = [textToShare, myWebsite]
let activityVC = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil)
var nav = UINavigationController(rootViewController: activityVC)
nav.modalPresentationStyle = UIModalPresentationStyle.Popover
var popover = nav.popoverPresentationController as UIPopoverPresentationController!
activityVC.preferredContentSize = CGSizeMake(500,600)
popover.sourceView = self.view
popover.sourceRect = CGRectMake(100,100,0,0)
self.presentViewController(nav, animated: true, completion: nil)
}
}
class MyViewController : UIViewController, UIPopoverPresentationControllerDelegate {
//code here
}

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