About
I would like to be able to enforce that some existential conforms to a specific protocol. This question relates to an existing package I am developing, which uses a sub-par solution, due to reduced compile time safety as I haven't found a way to express this within the Swift type system.
This post will use a SwiftUI example for demonstration purposes to give more context, but this is not dependent on SwiftUI. This post is a very simplied version of the problem I'm having.
Aim
I aim to have my package have full safety enforced by the type system. I will refer to the user side of the package as the "client". Requirements:
- The code in the framework should not (and will not) have access to protocols defined by the client.
- We use type safety instead of runtime checks.
Below is some code of what I expect the result to look like:
import SwiftUI
// Package
struct Tree<ID: Hashable>: View {
@Binding private var element: any Identifiable<ID>
init(element: Binding<any Identifiable<ID>>) {
_element = element
}
var body: some View {
Text("ID: \(element.id)")
}
}
// Client
protocol MyElement: Identifiable<UUID> {}
struct El1: MyElement {
let id = UUID()
}
struct El2: MyElement {
let id = UUID()
}
struct ContentView: View {
@State private var element: any MyElement = El1()
var body: some View {
Tree(element: $element)
}
}
This gives the following errors in the ContentView
body:
Cannot convert value of type 'Binding' to expected argument type 'Binding<any Identifiable>'
Generic parameter 'ID' could not be inferred
Explicitly specifying the ID
fixes the second error, but still not the first.
Current solution
We've seen what I aim for. Here's what I currently have, which does in fact work:
import SwiftUI
// Package
struct Tree<ID: Hashable>: View {
@Binding private var element: any Identifiable<ID>
init<T>(element: Binding<T>) {
precondition(element.wrappedValue is any Identifiable<ID>, "Element is of wrong type")
_element = Binding(
get: { element.wrappedValue as! any Identifiable<ID> },
set: { element.wrappedValue = $0 as! T }
)
}
var body: some View {
Text("ID: \(element.id)")
}
}
// Client
protocol MyElement: Identifiable<UUID> {}
struct El1: MyElement {
let id = UUID()
}
struct El2: MyElement {
let id = UUID()
}
struct ContentView: View {
@State private var element: any MyElement = El1()
var body: some View {
Tree<UUID>(element: $element)
}
}
It has several downsides:
- The
ID
type is explicitly required, even thoughMyElement
defines theID
. - Any type can be used in place of
T
, and so theprecondition
only catches issues at run time.
Solution
What's the solution to this? Does it currently exist within the Swift type system to express that an any
existential should have a value conforming to a certain protocol?