I want to constrain two generic types together by their associatedtypes, so I can compile time check for paired protocol matching instead of doing a runtime check for something like this:
func bind<V: View, P: Presenter>(_ v: V.Type, _ p: P.Type) where V: P.View, P: V.Presenter {
// we know V & P are mutually configurable
}
// implemented with an @ unavailable to give a more helpful compiler error than not implementing it at all
@available(*, unavailable, message: "Mismatched V.Presenter <-> P.View. Double check the View type conforms to Presenter.View, and the Presenter type conforms to View.Presenter")
func bind<V: View, P: Presenter>(_ v: V.Type, _ p: P.Type) {
// we know V & P are _not_ mutually configurable
}
protocol View {
associatedtype Presenter: AnyProtocol
}
protocol Presenter {
associatedtype View: AnyProtocol
}
I'm already able to solve this with an ugly runtime check:
func bind<V: View, P: Presenter>(_ v: V.Type, _ p: P.Type) where V: P.View, P: V.Presenter {
guard P.self is V.Presenter.Type, V.self is P.UserInterface.Type else {
assertionFailure(
"Mismatched V.Presenter <-> P.View. Double check \(V.self) type conforms to \(P.self).View, and \(P.self) type conforms to \(V.self).Presenter"
)
return
}
// safe to proceed binding V & P together
}
But this feels like something meant for the Generics system
Right now this isn't possible because I can't guarantee the associated types won't be fulfilled with a struct and AnyProtocol doesn't actually exist
Aside from bumping that I think AnyProtocol would be useful, is there a way to get the generic system to do something like this now?
This should work. bind has only one generic parameter, and you cannot make mismatched views/presenters because there are where clauses that ensure you get Self after roundtripping through types
func bind<V: View>(_ v: V, _ p: V.P) {
// we know V & P are mutually configurable
}
protocol View {
associatedtype P: Presenter where P.V == Self
}
protocol Presenter {
associatedtype V: View where V.P == Self
}
struct SampleView: View {
typealias P = SamplePresenter
}
struct SamplePresenter: Presenter {
typealias V = SampleView
}
//struct MismatchedView: View { // error: 'View' requires the types 'MismatchedView' and 'SamplePresenter.V' (aka 'SampleView') be equivalent
// typealias P = SamplePresenter
//}
//struct MismatchedPresenter: Presenter { // error: 'Presenter' requires the types 'MismatchedPresenter' and 'SampleView.P' (aka 'SamplePresenter') be equivalent
// typealias V = SampleView
//}
That doesn't compile unless the typealias'es go to concrete types unfortunately. Right now we can't bind an associatedtype to always be AnyProtocol, which leaves us open to fulfilling Presenter.View with a struct or something, and make a statement like V: Presenter.View invalid.
More concretely, I'm trying to make some compile time VIPER helpers, something to help a scenario like like:
protocol SampleViewInterface: View {}
struct SampleView: SampleViewInterface {
typealias P = SamplePresenterInterface
}
protocol SamplePresenterInterface: Presenter {}
struct SamplePresenter: SamplePresenterInterface {
typealias V = SampleViewInterface
}
struct SampleModuleBuilder {
func build() {
let presenter = SamplePresenter()
let view = SampleView()
presenter.view = view
view.presenter = presenter
//...
}
//... extension in Tests target or something
func buildPresenterAndMocks() {
let presenter = SamplePresenter()
let view = MockView()
presenter.view = view
view.presenter = presenter
//...
}