I'm looking to create a protocol upon View
to group views that can be constructed from some specific data struct (MyData
)
protocol EncodableView: View {
/// Returns true if this view type can be decoded from data
static func canDecode(from data: MyData) -> Bool
/// Constructs a view from the given data
init(from data: MyData) throws
}
I want to use this protocol in a view to route to different views based on their ability to decode MyData
:
struct Example: EncodableView {
// ... implements EncodableView
}
struct ContentView: View {
private var encodableViews: [any EncodableView.Type] = [
ExampleView.self,
// ... others
]
private func navigationDestination(for data: MyData) -> some View {
for (type in encodableViews) {
// Compile complains: Type 'any EncodableView' cannot conform to 'View'
if (type.canDecode(data)) {
return type.init(from: data)
}
}
return EmptyView()
}
var body: some View {
NavigationStack {
VStack {
// ...
}
.navigationDestination(for: MyData.self) { data in
navigationDestination(for: data)
}
}
}
}
However, I'm having trouble finding the right combination of some View
, any View
, AnyView
and generics to achieve this.
I've marked the spot in my code snippet above where the compile complains: Type 'any EncodableView' cannot conform to 'View'
One Solution but not ideal because while checking the suffix
is fast you wouldn't want to check every time the view is redrawn.
extension URL {
@ViewBuilder var view : some View {
if self.lastPathComponent.hasSuffix("mp4") {
Text("shoe video player")
} else if self.lastPathComponent.hasSuffix("png") {
AsyncImage(url: self)
} else {
Text("unsupported")
}
}
}
Then you can use it something like
url.view
but like I mentioned above the decision will be happening multiple times, views shouldn't be deciding.
You could decide before and then tell the View
what it is and what to show.
enum URLTypes: Hashable, Codable {
case mp4(URL)
case png(URL)
///Creates a type
static func decode(url: URL) -> Self {
if url.lastPathComponent.hasSuffix("mp4") {
return .mp4(url)
} else if url.lastPathComponent.hasSuffix("png") {
return.png(url)
} else {
return .unknown(url)
}
}
//What to show
@ViewBuilder var view: some View {
switch self {
case .mp4(let uRL):
Text("show video player \(uRL)")
case .png(let uRL):
AsyncImage(url: uRL)
case .unknown(let uRL):
Text("unsupported \(uRL)")
}
}
}
And use it in your navigationDestination
.navigationDestination(for: URLTypes.self) { data in
data.view
}
But you could use generics too just be concrete when you get to the View
struct MyData<Content>: Hashable, Codable where Content: Hashable & Codable {
let value: Content
init(double: Double) where Content == Double {
value = double
}
init(string: String) where Content == String {
value = string
}
init(int: Int) where Content == Int {
value = int
}
init(url: URL) where Content == URLTypes {
value = URLTypes.decode(url: url)
}
}
And in the navigationDestination
.navigationDestination(for: MyData<URLTypes>.self) { data in
data.value.view
}
.navigationDestination(for: MyData<Double>.self) { data in
Text(data.value, format: .number.precision(.fractionLength(2)))
}
The more decisions and processing your put on a View
/main thread/ main actor the slower and more sluggish your app will be.