2

I have found a weird issue with SwiftUI's ForEach (and List) where if you use an Array of subclass types where the parent class implements BindableObject, the ForEach loop insists each item is of the base class type not the Subclass you are using, see my example code below. A little experimenting has found if the subclass implements BindableObject then the issue goes away, which in the example I have shown is OK, but often is not really suitable.

Anybody seen this know how you are suppose to deal with this or perhaps this is a bug and I should raise it with Apple?

class Bar: BindableObject {
  let didChange = PassthroughSubject<Bar, Never>()

  let   name: String
  init(name aName: String) {
    name = aName
  }
}

class Foo: Bar {
  let   value: Int
  init(name aName: String, value aValue: Int) {
    value = aValue
    super.init(name:aName)
  }
}

let   arrayOfFoos: Array<Foo> = [ Foo(name:"Alpha",value:12), Foo(name:"Beta",value:13)]

struct ContentView : View {
  var body: some View {
    VStack {
      ForEach(arrayOfFoos) { aFoo in
        Text("\(aFoo.name) = \(aFoo.value)")    // error aFoo is a Bar not a Foo
      }
    }
  }
}

1 Answer 1

2

Tried this on Xcode Beta 2

I think this is not a bug but rather a "feature" of Swift type system and SwiftUI API.

If you look at the signature of ForEach (just Cmd + Click on ForEach)

public init(_ data: Data, content: @escaping (Data.Element.IdentifiedValue) -> Content)

you can notice that it accepts Data.Element.IdentifiedValue type

So, from your example

struct ContentView : View {
  var body: some View {
    VStack {
      ForEach(arrayOfFoos) { aFoo in
        Text("\(aFoo.name) = \(aFoo.value)")    // error aFoo is a Bar not a Foo
      }
    }
  }
}

aFoo local value has type Foo.IdentifiedValue

Lets ask Swift what it thinks about this type:

Foo.IdentifiedValue.self == Bar.IdentifiedValue.self // true
Foo.IdentifiedValue.self == Foo.self // false
Foo.IdentifiedValue.self == Bar.self // true

As you can see, Foo.IdentifiedValue is actually Bar.

To bypass this we can create a wrapper using a new feature of Swift 5.1 - 'Key Path Member Lookup'! :D

I updated your example. Added AnyBindable class and mapped elements of arrayOfFoos to it.

class Bar: BindableObject {
    let didChange = PassthroughSubject<Void, Never>()

    let   name: String
    init(name aName: String) {
        name = aName
    }
}

class Foo: Bar {
    let value: Int
    init(name aName: String, value aValue: Int) {
        value = aValue
        super.init(name:aName)
    }
}

@dynamicMemberLookup
class AnyBindable<T: BindableObject>: BindableObject {
    let didChange: T.PublisherType

    let wrapped: T

    init(wrapped: T) {
        self.wrapped = wrapped
        self.didChange = wrapped.didChange
    }

    subscript<U>(dynamicMember keyPath: KeyPath<T, U>) -> U {
        return wrapped[keyPath: keyPath]
    }
}

let arrayOfFoos = [ Foo(name:"Alpha",value:12), Foo(name:"Beta",value:13)]
    .map(AnyBindable.init)

struct ContentView : View {
    var body: some View {
        VStack {
            ForEach(arrayOfFoos) { aFoo in
                Text("\(aFoo.name) = \(aFoo.value)")    // it compiles now
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.