I've got an @Published protocol array that I am looping through with a ForEach to present the elements in some view. I'd like to be able to use SwiftUI bindable syntax with the ForEach to generate a binding for me so I can mutate each element and have it reflected in the original array.
This seems to work for the properties that are implemented in the protocol, but I am unsure how I would go about accessing properties that are unique to the protocol's conforming type. In the example code below, that would be the Animal's owner property or the Human's age property. I figured some sort of type casting might be necessary, but can't figure out how to retain the reference to the underlying array via the binding.
The other thing I wanted to do was use protocol composition to combine Testable and Identifiable such that I could use it with ForEach but I can't figure out how to do that either. (i.e., typealias TestableIdentifiable = Testable & Identifiable)
Let me know if you need more detail.
import SwiftUI
protocol Testable {
var id: UUID { get }
var name: String { get set }
}
struct Human: Testable {
let id: UUID
var name: String
var age: Int
}
struct Animal: Testable {
let id: UUID
var name: String
var owner: String
}
class ContentViewModel: ObservableObject {
@Published var animalsAndHumans: [Testable] = []
}
struct ContentView: View {
@StateObject var vm: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
ForEach($vm.animalsAndHumans, id: \AnyTestable.id) { $object in
TextField("textfield", text: $object.name)
// if the object is an Animal, how can I get it's owner?
}
Button("Add animal") {
vm.animalsAndHumans.append(Animal(id: UUID(), name: "Mick", owner: "harry"))
}
Button("Add Human") {
vm.animalsAndHumans.append(Human(id: UUID(), name: "Ash", age: 26))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can achieve this through downcasting:
extension Testable {
// This is to be used when Testable is _known_
// to be an Animal
var forceAsAnimal: Animal {
get { self as! Animal }
set { self = newValue }
}
}
So then, In your TextField body:
TextField("textfield", text: $object.name) {
if object is Animal {
var animal: Binding<Animal> = $object.forceAsAnimal
}
}
However, this approach isn’t very efficient and is error prone. I’d recommend using an enum when wanting to store a couple of different types:
enum Testable {
case human(Human), animal(Animal)
var id: UUID {
switch self {
case let .human(human): return human.id
case let .animal(animal): return animal.id
}
}
var name: String { ... }
}
Evidently, this setup has a lot of boilerplate, since every common property needs to be computed on Testable with a switch statement. If you find such repetition across all types stored in your enum, you can just move these properties to Testable:
struct Testable {
let id: UUID
var name: String
enum Type {
case human(age: Int)
case owner(owner: String)
}
var type: Type
}
I hope this helped! Please let me know if something was unclear.
-Filip
1 Like