swiftgenericsprotocols

How can I inject generic data into a view controller?


I am trying to inject generic data into a View Controller using a generic parameter T. I have a protocol Coordinator which declares an associated type T and a data variable of type T. My AppCoordinator adopts the Coordinator protocol and declares the type of T as a String. In my AppCoordinator class, I am trying to set the data on a view controller using the generic parameter T.

My thinking is that the class WelcomeViewController adopts a concrete type of AppData for data when it implements the Navigable protocol, and so I should be able to set the type of the data in the AppCoordinator, which is String in this case, and be able to set that on the view controller as well. Somehow I suppose that in order for this to work the type T in the Coordinator protocol must match the type T in the Navigable protocol but when I try to do this by setting associated type T: Navigable.T in the Coordinator protocol, for example, I get this error: "Cannot access associated type 'T' from 'Navigable'; use a concrete type or generic parameter base instead".

I have read How do I inject dependencies into an iOS view controller?, and the sections: The problems that generics solve, Generic Functions, Type Parameters, Naming Type Parameters, Type Constraints, Associated Types, Generic Where Clauses, Associated Types with a Generic Where Clause, from Generics.

How can I solve this?

protocol Coordinator<T> {
    associatedtype T
    var navigationController: UINavigationController { get }
    var data: T? { get }
    func start<T>(data: T?, viewController: any Navigable)
}

class AppCoordinator: Coordinator {

    typealias T = String

    var data: T?
    
    init(data: T? = nil, navigationController: UINavigationController) {
        self.data = data
        self.navigationController = navigationController
    }
    
    var navigationController: UINavigationController
    
    func start<T>(data: T?, viewController: any Navigable) {
        var viewController = viewController
        viewController.data = data
    }
}

protocol Navigable<T> where T == AppData {
    associatedtype T
    var data: T? { get set }
}

enum AppData {
    case coordinatorData
}

class WelcomeViewController: UIViewController, Navigable {
    var data: AppData?
    var appCoordinator: (any Coordinator)?
    
    typealias T = AppData
    
    override func viewDidLoad() {
        setUp()
    }
}

Solution

  • You can add generic constraint to your start function to acheive your goal:

    protocol Coordinator<T> {
        associatedtype T
        var navigationController: UINavigationController { get }
        var data: T? { get }
        func start<U: Navigable>(data: T?, viewController: U) where U.T == T //<- constraint here
    }
    
    class AppCoordinator: Coordinator {
    
        typealias T = AppData
        typealias U = WelcomeViewController
        
        var data: AppData?
        
        init(data: T? = nil, navigationController: UINavigationController) {
            self.data = data
            self.navigationController = navigationController
        }
        
        var navigationController: UINavigationController
        
        func start<U>(data: AppData?, viewController: U) where U : Navigable, AppData == U.T {
            var viewController = viewController
            viewController.data = data
        }
    }
    
    protocol Navigable { // <- remove T == AppData here
        associatedtype T
        var data: T? { get set }
    }
    
    enum AppData {
        case coordinatorData
    }
    
    class WelcomeViewController: UIViewController, Navigable {
        var data: AppData?
        var appCoordinator: (any Coordinator)?
        
        typealias T = AppData
        
        override func viewDidLoad() {
            //setUp()
        }
    }
    

    Now you can use your Coordinator like this:

    var coor: any Coordinator<AppData> // <- can swap to any concrete type that conform Coordinator with T = AppData
    coor = AppCoordinator(navigationController: UINavigationController())
    coor.start(data: .coordinatorData, viewController: WelcomeViewController())