I want to subclass ViewControllers to create a generic Coordinator class. This coordinator class should be able to type safely inject dependencies to child coordinator classes. Child coordinators should only have access to explicitly defined dependencies. I made the following working playground with abstract classes to layout the problem. I am open to other ideas how to solve the described problem.
import Foundation
protocol HasFirstDependency {
var first: Any? { get }
}
protocol HasSecondDependency {
var second: Any? { get }
}
typealias AllDependencies = HasFirstDependency & HasSecondDependency
struct AppDependencies: AllDependencies {
var first: Any?
var second: Any?
}
class Coordinator<D> {
var dependencies: D?
}
extension Coordinator {
static func initialize() -> Coordinator<D> {
return Coordinator<D>()
}
}
class ParentCoordinator: Coordinator<AllDependencies> {
var children: [Any] = []
}
class FirstChildCoordinator: Coordinator<HasFirstDependency> {}
class SecondChildCoordinator: Coordinator<HasSecondDependency> {}
The following code outlines the question (see comments 1. and 2.). Is there anyway to avoid casting and restrict the childType at compile time in the described way?
extension ParentCoordinator {
func add<C: Coordinator<D>, D>(childType: C.Type) { // 2. ... by setting a constraint like this: "where self.dependecies is D?"
let child = C.initialize()
children.append(child)
if let dependencies: D? = self.dependencies as? D? { // 1. is there any way to avoid this casting ...
child.dependencies = dependencies
} else {
assertionFailure("parentCoordinator does not hold child dependencies")
}
}
}
let parent = ParentCoordinator()
parent.dependencies = AppDependencies(first: "bla", second: "blup")
parent.add(childType: FirstChildCoordinator.self)
let child = parent.children.first as? Coordinator<HasFirstDependency>
print(type(of: parent.dependencies)) // Optional<HasFirstDependency & HasSecondDependency>
print(parent.dependencies?.first) // Optional("bla")
print(parent.dependencies?.second) // Optional("blup")
print(type(of: child?.dependencies)) // Optional<HasFirstDependency>
print(child?.dependencies?.first) // Optional("bla")
//print(child?.dependencies?.second) // does not compile, what I wanted to achieve
To be more specific: The following fatal error is, what I want to catch at compile time.
protocol ForgottenDependency {
var forgotten: Any? { get }
}
class ThirdChildCoordinator: Coordinator<ForgottenDependency> {}
parent.add(childType: ThirdChildCoordinator.self) // Fatal error: parentCoordinator does not hold child dependencies: file MyPlayground.playground, line 49
What you are requesting is some kind of logical OR
on type constraints:
... where D == HasFirstDependency || HasSecondDependency
Disjunction of type constraints is not possible in swift. To quote the commonly rejected changes:
Disjunctions (logical ORs) in type constraints: These include anonymous union-like types (e.g. (Int | String) for a type that can be inhabited by either an integer or a string). "[This type of constraint is] something that the type system cannot and should not support."
One possible solution can be to make dependencies conform to a common protocol. This, to be honest, does not exactly solve the problem of detecting "forgotten" dependencies, since all of the dependencies would have to implement this common protocol. This approach was discussed here.