I'm a Swift/SwiftUI beginner and am trying to make a program that visualizes a few different types of data (w/ swift 5.7, targeting macOS).
I have a root "Data
" protocol, and a few different protocols for its subtypes. A simplified example:
protocol Data {}
protocol HasA: Data { var a: String {get} }
protocol HasB: Data { var b: String {get} }
struct StructA: HasA { let a: String = "I am A"}
struct StructB: HasB { let b: String = "I am B"}
Now, I'd like to have a view that picks a visualization strategy based on the specific type of data it receives. This doesn't work, but here's how I'd write in a dynamic language:
struct DataView: View {
let data: Data
var body: some View {
// this doesn't actually work
if data is HasA {
ViewA(data) // ERROR: type 'any Data' does not conform to the expected type 'HasA'
} else if data is {
ViewDefault(data)
}
}
}
I've tried many, many different ways to accomplish this, without success. So clearly, there's something fundamentally wrong with my strategy. How should I be approaching this problem? Generics? Using concrete classes only? More existential types?
Some things I've tried that didn't work
Generics in many different forms, all of which ended up with
type 'any Data' does not conform to expected type 'Data'
errors.Protocol extensions of the form:
extension Data { func getView() -> View { ...} } extension HasA { func getView() -> View { ...} }
This will compile, but at runtime, it only calls the
Data.getView()
implementation, not theHasA.getView()
implementation.Adding a required
func getView() -> any View
method to theData
protocol, and then implementing it on the concrete classes. This leads toType 'any View' cannot conform to 'View'
errors.Runtime dispatch via protocol conformance - this is what's in the example above, i.e.,
if data is HasA {return ViewA(data)}
. I didn't really expect this to work, and it doesn't - the compiler complains about protocol conformance.
Edited to add
In addition to @EDUsta's solution below, in my real production solution, I also had to remove all associated types and generic parameters from the data protocols that the view layer uses - otherwise downcasting wouldn't work.
CodePudding user response:
With your current approach, at some point, you need to know the underlying type/protocol to act upon it. If you'd like to continue with this approach, this would work out of the box:
struct DataView: View {
let data: Data
var body: some View {
switch data {
case is HasA:
ViewA(data: data as! HasA)
default:
EmptyView()
}
// or
if let data = data as? HasA {
ViewA(data: data)
} else {
EmptyView()
}
}
}
The reason is, is HasA
is not enough for a (smart) cast, like Kotlin.
I'd suggest another approach with SwiftUI though, AnyView
! AnyView
erases the type of the View
, so you can combine this with your third approach:
protocol NVData {
func getView() -> AnyView
}
protocol HasA: NVData {
var a: String { get }
}
protocol HasB: NVData {
var b: String { get }
}
extension HasA {
func getView() -> AnyView {
AnyView(ViewA(data: self))
}
}
extension HasB {
func getView() -> AnyView {
AnyView(EmptyView())
}
}
struct StructA: HasA { let a: String = "I am A" }
struct StructB: HasB { let b: String = "I am B" }
struct DataView: View {
let data: any NVData
var body: some View {
data.getView()
}
}
struct ViewA: View {
let data: HasA
var body: some View {
Text(data.a)
}
}
Deriving the View
from the Data
inverts the dependency, though. Something to keep in mind.