Using a protocol array with ForEach and bindable syntax

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