Nick N
Nick N

Reputation: 1048

Using Swift Generics in Base class to easily return type class to subclass

I want to hold onto a Interactor class in a BasePresenter class, but for the life of me I can't figure out how on earth to use generics easily in Swift. I want to have a function in the base class (like BasePresenter interactorForType).

public class InboxListPresenter: BasePresenter, ObservableObject {
        
    public init(interactor: InboxListInteractor) {
        
        super.init(router: InboxListRouter(viewData: viewData), interactor: interactor)
    }
    
    func findInboxNotifications() {
        
        interactorForType(type: InboxListInteractor.self).findInboxNotifications() { inboxNotifications, errorCode in
            
            // do something
        }
    }    
}

Here's the BasePresenter where I want to hold the generic object. I'm trying to use protocols (ILibertyInteractor)

open class BasePresenter {
    
    public let router:ILibertyRouter
    public let interactor:ILibertyInteractor
    
    public init(router:ILibertyRouter, interactor:ILibertyInteractor) {
        self.router = router
        self.interactor = interactor
    }
            
    func interactorForType<T>(type: T.Type) -> T {
        return interactor as! T
    }
    
    func routerForType<T>(type: T.Type) -> T {
        return router as! T
    }
}

The above interactorForType does a force unwrap on interaction that I want to avoid. Also, I don't really want to pass in the object either. I'd like to use protocols and generics to have a generic function that returns type T.

Below is the start of what I was trying to allow returning the type back within the presenter above.

public protocol ILibertyPresenter {
    var router:ILibertyRouter { get }
}

public protocol ILibertyRouter {
    
}

public protocol ILibertyInteractor {
//    associatedtype T
    
//    func trueInteractor() -> T
}

Upvotes: 2

Views: 1699

Answers (1)

Chip Jarred
Chip Jarred

Reputation: 2785

There are two things here. There is the question you ask, "how to store a generic?", and then there is what your example code seems to be trying to do.

How to store a generic value

The short answer for how to store a generic (assuming going through Any is undesirable - and it is undesirable), is to store it in a generic.

If want to hold on to a generic type as its explicit type, the thing holding onto it also have to be generic and specialized on the same type. That would require your InboxListPresenter to be generic too. So you could do this:

open class BasePresenter <InteractorType: IteractorProtocol>{
    
    public let router:ILibertyRouter
    public let interactor: InteractorType // You can do this now
    
    public init(router:ILibertyRouter, interactor:ILibertyInteractor) {
        self.router = router
        self.interactor = interactor
    }
            
    /* you don't need this now, because InteractorType is part of the generic class definition 
    func interactorForType<T>(type: T.Type) -> T {
        return interactor as! T
    }
    */
    
    func routerForType<T>(type: T.Type) -> T {
        return router as! T
    }
}

In this example, I specified that IteractorType conforms to InteractorProtocol.

Under some circumstances you eliminate the need for the surrounding type to be generic, allowing you to do this:

open class BasePresenter{
    
    public let router:ILibertyRouter
    public let interactor: InteractorProtocol
    
    public init(router:ILibertyRouter, interactor:InteractorProtocol) {
        self.router = router
        self.interactor = interactor
    }
            
    /* you don't need this now, because InteractorProtocol should define the interface for all Interactors 
    func interactorForType<T>(type: T.Type) -> T {
        return interactor as! T
    }
    */
    
    func routerForType<T>(type: T.Type) -> T {
        return router as! T
    }
}

Specifically you can do that if InteractorProtocol doesn't have, as the dreaded Swift compiler error will say, "Self or associated type constraints". Basically if you have where clauses or other protocol conformance constraints on the protocol or on the associated types in the protocol, then you can only use the protocol to constrain generic parameters.

If you it doesn't have Self or associated type constraints, then you can use it as though it were an ordinary type, including as the type for a stored property.

What the code seems to want

The code you've shown is basically a poster child for classic OOP-style inheritance. That's not the only way to solve the problem, but looking at it, it screams for an Interactor base class that defines the interface, and for concrete implementations to do the actual work. I don't know what Interactor has to do, but if it's sufficiently complex, you can apply all sorts of design patterns to avoid having to have a gazillion subclasses of it. And you can borrow the idea from Cocoa/CocoaTouch of declaring delegate protocols so you can modify the behavior that way.

Anyway the inheritance approach is fairly straight forward.

open class Iteractor {
    // Just replace the closure parameter types with whatever they're supposed to be
    func findInboxNotifications((Any, ErrorCode) -> Void) { fatalError("Implement in subclass!") }
}

Then you use that as your base class for your concrete Interactor types.

final class AConcreteInteractor: Iteractor {
    override func findInboxNotifications ((Any, ErrorCode) -> Void)  {
        /* Do actual work here */ 
    }
}

Then in your BasePresenter:

open class BasePresenter {
    
    public let router:ILibertyRouter
    public let interactor: Iteractor
    
    public init(router:ILibertyRouter, interactor: Iteractor) {
        self.router = router
        self.interactor = interactor
    }
            
    /* This isn't needed because you'll use `interactor` polymorphically`
    func interactorForType<T>(type: T.Type) -> T {
        return interactor as! T
    }
    */

    ...
}

InboxListPresenter would then look like this

public class InboxListPresenter: BasePresenter, ObservableObject {
        
    public init(interactor: InboxListInteractor) {
        
        super.init(router: InboxListRouter(viewData: viewData), interactor: interactor)
    }
    
    func findInboxNotifications() {
        
        iteractor.findInboxNotifications() { inboxNotifications, errorCode in
            
            // do something
        }
    }    
}

Using Protocols instead of Inheritance

I don't think protocols is a good fit in this case, but in some ways, protocols can substitute for class-based inheritance; but since you have to store the interactor, that puts you right back at having to make sure you don't have Self or associated type constraints.

When used this way the only advantage to protocols over inheritance is that you get some better compiler checks - for example, it can ensure that all required implementation methods are actually implemented by the types conforming to the protocol, whereas in class inheritance, the base class always defines them (because unlike Java, Swift doesn't let you define a purely abstract base class, nor does it allow you specify "pure" virtual methods likes C++). You pay for that benefit in being more constrained how you define them so that you can use them as stored property types.

I would say that the code you present in your question wants to use class-based inheritance for your Interactors. Unless you have some good reason not to, I would just go with that rather than try to force the square peg into a round hole.

Upvotes: 2

Related Questions