Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Generics: function with T.Type as parameter returns optional T

I'm attempting to write a series of generic functions that will sort through a stack of viewControllers by passing a UIViewController class or subclass Type and then either return the instance of the "found" viewController or nil. So far I have been unable to even get this simple snippet to compile:

extension UINavigationController {

    func fhk_find<T: UIViewController>(viewControllerType: T.Type) -> T?
    {
        if let viewController = viewControllers.first as? viewControllerType {
            return viewController
        }
        else {
            return nil
        }
    }
}

and would call:

navController.fhk_find(fooViewController.self)

However, the compiler is telling me that viewControllerType is not a type.

I'm not exactly sure what I'm missing here...

like image 432
Stephen Newton Avatar asked Jan 29 '16 16:01

Stephen Newton


People also ask

How do I call a generic function in Swift?

Solution. A generic function that you might need to use explicit specialization is the one that infer its type from return type—the workaround for this by adding a parameter of type T as a way to inject type explicitly. In other words, we make it infer from method's parameters instead.

What is a generic type parameter?

Generic Methods A type parameter, also known as a type variable, is an identifier that specifies a generic type name. The type parameters can be used to declare the return type and act as placeholders for the types of the arguments passed to the generic method, which are known as actual type arguments.

What is type parameter in Swift?

A type parameter is simply the name of a placeholder type (for example, T , U , V , Key , Value , and so on). You have access to the type parameters (and any of their associated types) in the rest of the type, function, or initializer declaration, including in the signature of the function or initializer.


1 Answers

You need to cast the viewControllers.first (in case it exist) to T, rather than to the parameter viewControllerType. In fact, you won't need to make use of this parameter at all; you can modify your extension to the following:

extension UINavigationController {
    func fhkFindFirst<T: UIViewController>(_: T.Type) -> T? {
        for viewController in viewControllers {
            if let viewController = viewController as? T {
                return viewController
            }
        }
        return nil
    }
}

Example usage follows below. Note that you call with fooViewController.dynamicType rather than fooViewController.self. The latter gives you the value of fooViewController.self rather than the type, i.e., fooViewController.self = fooViewController.self.

Now note, however, that an attempted conversion from a subclass type to its superclass will always succeed, so the solution above will correctly identify subclass instances (subclasses of UIViewController), whereas if you query for a superclass instance (i.e., T as superclass UIViewController) then viewController = viewController as? T will always succeed even if viewController is in fact an instance of a subclass of UIViewController.


Using fhkFindFirst(...) to identify subclass instances: OK

In the following example two subclass instances are correctly identified and their references returned to caller.

class FooViewController : UIViewController { }
class BarViewController : UIViewController { }

let fooViewController = FooViewController()
let barViewController = BarViewController()

let navController = UINavigationController(rootViewController: fooViewController)
navController.addChildViewController(barViewController)

print(navController.fhkFindFirst(fooViewController.dynamicType) ?? "None found.")
// or: print(navController.fhkFindFirst(FooViewController))
/* <__lldb_expr_1582.FooViewController: 0x7fb97840ad80> */

print(navController.fhkFindFirst(barViewController.dynamicType) ?? "None found.")
// or: print(navController.fhkFindFirst(BarViewController))
/* <__lldb_expr_1582.BarViewController: 0x7fb978709340> */

Using fhkFindFirst(...) to find superclass instances: not as intended

Whereas in the following case, fooViewController is an UIViewController object, and is mis-identified as a BarViewController, since type conversion of of BarViewController object (in UINavigationController.viewControllers) to UIViewController succeeds.

class BarViewController : UIViewController { }

let fooViewController = UIViewController()
let barViewController = BarViewController()

let navController = UINavigationController(rootViewController: barViewController)
navController.addChildViewController(fooViewController)

print(navController.fhkFindFirst(fooViewController.dynamicType) ?? "None found.")
// or: print(navController.fhkFindFirst(UIViewController))
    /* <__lldb_expr_1533.BarViewController: 0x7fa519e2bd40> <-- "wrong" one */

print(navController.fhkFindFirst(barViewController.dynamicType) ?? "None found.")
// or: print(navController.fhkFindFirst(BarViewController))
    /* <__lldb_expr_1533.BarViewController: 0x7fa519e2bd40> */

To wrap up; the above will work as long as you only use the method to search for subclasses of UIViewController; whereas if you try to search for a superclass object, you will get the first controller in UINavigationController.viewControllers.


Addition with regard to edelaney05:s related question in comments below

I'll start by quoting the question in case the comment were to be deleted:

edelaney05: Is there a way to guard against this? This is a pretty interesting edge case to consider especially if you've got an intermediate class. class FooVC: AwesomeVC { ... }, class BarVC: AwesomeVC { ... }, and class AwesomeVC: UIViewController { ... }.

Yes, you can work around this behaviour, in case you know you'll be working with other than only pure (1st-level) subclasses of UIViewController; say, to allow for finding first instance of

  • Subclasses to UIViewController. (+)
  • Subclasses to these subclasses. (++)

We can make use of dynamic type comparison to ensure that we don't perform a conversion of a subclass instance to its superclass (and hence misidentifying a subclass instance as the superclass instance we're looking for).

extension UINavigationController {
    func fhkFindFirst<T: UIViewController>(_: T.Type) -> T? {
        for viewController in viewControllers {
            if let supClassType = viewController.superclass?.dynamicType where supClassType != T.Type.self {
                if let viewController = viewController as? T {
                    return viewController
                }
            }
        }
        return nil
    }
}

class FooBarViewController: UIViewController { }

class FooViewController : FooBarViewController { }
class BarViewController : FooBarViewController { }

let fooBarViewController = FooBarViewController()
let fooViewController = FooViewController()
let barViewController = BarViewController()

let navController = UINavigationController(rootViewController: fooViewController)
navController.addChildViewController(barViewController)
navController.addChildViewController(fooBarViewController)

print(navController.fhkFindFirst(FooViewController) ?? "None found.")
/* <__lldb_expr_1582.FooViewController: 0x7fe22a712e40> */

print(navController.fhkFindFirst(BarViewController) ?? "None found.")
/* <__lldb_expr_1582.BarViewController: 0x7fe22a4196a0> */

print(navController.fhkFindFirst(FooBarViewController) ?? "None found.")
/* <__lldb_expr_1582.FooBarViewController: 0x7fe22a70ee60> */
like image 69
dfrib Avatar answered Oct 22 '22 20:10

dfrib