aryaxt
aryaxt

Reputation: 77596

swift method parameter of type that implements a protocol?

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

Upvotes: 0

Views: 247

Answers (2)

par
par

Reputation: 17724

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

Upvotes: 1

Rob Napier
Rob Napier

Reputation: 299355

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.

Upvotes: 3

Related Questions