0

Trying to achieve something like the following objective c code in swift UIViewController<Routable> but it complains that it can't infer the generic type

I think it wants me to somehow define what subclass of UIViewController to use, but I don't care. I just want a UIViewController that implements Routable

public protocol Routable {
    static func provideInstance(id: String?) -> Self?
}

public class Router {
    public func getDestinationViewController<T: UIViewController>(url: URL) -> T? where T: Routable {
        return nil
    }
}

// Generic parameter `T` could not be inferred
let vc = Router().getDestinationViewController(url: URL(string: "www.domain.com/users/1")!)

I know i could use casting to solve the problem and just have the method return either a UIViewController or a Routable, but I rather do it the right way

aryaxt
  • 76,198
  • 92
  • 293
  • 442

2 Answers2

3

What precise type do you expect vc to be in this case? (I mean type determinable at compile time, looking at above code, not "some type we don't know until runtime.") If you can't answer that, the compiler can't either.

The right way here is to "return either a UIViewController or a Routable," exactly as you say. That's everything you know at this point. Trying to say something more precise would be incorrect.

The fact that vc is type-inferred is just a convenience. It still has to have a specific, known type at compile-time. If you can't write it in the source, the compiler can't infer it.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • is it really not possible to return a UIViewController that is also Routable? This was possible in objc `- (UIViewController *)gimeeSomething` – aryaxt Nov 30 '16 at 18:22
  • 1
    It is not possible to express this in Swift. You would have to add whatever particular methods you want from `UIViewController` onto the `Routable` protocol. This is a well-known limitation and will likely be improved in future versions of Swift, but currently it's impossible. – Rob Napier Nov 30 '16 at 18:31
1

When you use generic functions the type of the generic parameter is inferred from the type of the variable you assign the result to (or, in the case of a function that returns Void, from the concrete type of a parameter you pass to an argument of type T).

In your example, you do not tell the compiler what type your vc variable is so you get an error. You can fix this easily like so:

class MyViewController: UIViewController, Routable {
    public static func provideInstance(id: String?) -> Self? {
        return self.init()
    }
}

// give `vc` a concrete type so `T` can be inferred:
let vc: MyViewController? = Router().getDestinationViewController(url: URL(string: "www.domain.com/users/1")!)

EDIT:

Since your question seems to be more along the lines of "How do I replicate Objective-C's notion of UIViewController *<Routable>", which you can't do in Swift, I'll mention that you might find it worthwhile to review your design. When I moved from Objective-C to Swift I thought not being able to use something like UIViewController *<Routable> would be hard to get around (and even filed a bug report with Apple about it) but in practice it hasn't been an issue.

Your Router class needs some information in your view controller to be able to route properly. In your example you're looking for a view controller that is associated with a particular URL. The implication is that your UIViewController has a property that contains this URL, so rather than return a Routable protocol, the correct approach is to subclass UIViewController like so:

class RoutableViewController: UIViewController {
    let url: URL = URL(string: "...")
}

Now your router looks like:

class Router {
    var routables = [RoutableViewController]()

    func viewControllerWithURL(_ url: URL) -> RoutableViewController? {
        return routables.first
    }
}

Now you don't need to do any type-casting, and type-safety is (obviously) maintained.

EDIT 2:

Here's a solution that makes every view controller conform to Routable (I don't think you gain much vs. the solution I proposed in my previous edit, but here it is anyway):

protocol Routable {
    var url: URL? { get }
}

// now every UIViewController conforms to Routable
extension UIViewController: Routable {
    var url: URL? { return nil }
}

class MyViewController: UIViewController {
    private let myURL: URL

    override var url: URL? { return myURL }

    init(url: URL) {
        myURL = url
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class Router {
    var viewControllers: [UIViewController] = [
        MyViewController(url: URL(string: "foo")!),
        UIViewController()
    ]

    func viewController(for url: URL) -> UIViewController? {
        for viewController in viewControllers {
            if viewController.url == url { return viewController }
        }

        return nil
    }
}
par
  • 17,361
  • 4
  • 65
  • 80
  • This doesn't work because I don't know what vc I'm gonna get back, I want it to be dynamic. The router goes through my registered routes and finds a match and returns it not knowing which vc it is – aryaxt Nov 30 '16 at 17:54
  • Then you want a function that returns `UIViewController?` and you'll have to typecast using either `as?` or multiple `case`s in a `switch` statement (e.g. `case is MyViewController:`). Generics are used to provide method implementations for as-yet unknown arguments, not to allow arbitrary downcasting. – par Nov 30 '16 at 18:03
  • "I know i could use casting to solve the problem and just have the method return either a UIViewController or a Routable, but I rather do it the right way" This was possible in objective c, I'm surprised Swift doesn't support this – aryaxt Nov 30 '16 at 18:20
  • I did read your question :) ... Swift is intended to be a much more type-safe language than Objective-C, so currently "the right way" is a solution that uses typecasts. See for example http://stackoverflow.com/a/26403660/312594. – par Nov 30 '16 at 18:29
  • I've updated my answer with an alternate approach that is type-safe. Please take a look. – par Nov 30 '16 at 18:47
  • The edit solution doesn't work because the protocol gives me the ability to create an extension of any viewContoroller and conform to the protocol to become routable and support deeplinks :( `extension ProfileviewController: Routable` so this solution uses my existing ViewContorllers and make them routable – aryaxt Nov 30 '16 at 18:52
  • I've added one more edit, this time showing how every view controller can be made `Routable`. Note though that the default implementation is meaningless (i.e. it just returns `nil`), but maybe that's what you want. For a view controller to be truly "routable", it has to return something meaningful for the `url` property, which the subclass does. This is another type-safe solution. – par Nov 30 '16 at 19:05
  • It's important to realize that what Objective-C's `UIViewController *` _means_ in a practical sense is "the subset of `UIViewController`'s that are `Routable`". When you think about it this way you can see you don't want _any_ view controller, you only want those that are `Routable` -- and those have to have some conformant (e.g. `url`) property. When you think about "a subset of view controllers with a particular property" it should become apparent that what you really want is a `Routable` *subclass* of `UIViewController`. – par Nov 30 '16 at 19:17
  • Thank you very much for the effort and good answer, my project contains vcs 1- in storyboard 2- in code 3- initialized from xib. that's why created a provideInstance instead of an init. Also I have 7-8 deeplinks I need to support and probably better not to initialize all of the VCs and keep them in memory. This router is responsible for detecting URLs from deeplink, and dynamically find the proper destination and present it – aryaxt Nov 30 '16 at 21:35