简体   繁体   中英

SwiftUI ForEach with Array of SubClasses

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
      }
    }
  }
}

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
            }
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM