Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
2.0k views
in Technique[技术] by (71.8m points)

swift - How to generate a binding for each array element

If I had a @State or an @ObservedObject variable with an array property, and I wanted to use List and pass a binding of each element of the array into some child View (e.g. Toggle or TextField), is there a standard way to do that?

Simplified example:

struct Person: Identifiable {
  var id: UUID = .init()
  var name: String
  var isFavorite: Bool = false
}

struct ContentView: View {
  @State var people = [Person(name: "Joey"), Person(name: "Chandler")]

  var body: some View {
     List(people) { person in
        HStack() {
           Text(person.name) 
           Spacer
           Toggle("", isOn: $person.isFavorite) // <- this obviously doesn't work
        }
     }
  }
}

This seems like a fairly common scenario, but I can't figure out an obvious solution aside from manually building a separate array of bindings.

The only elegant solution I came up with (I'll add it as an answer, if there isn't something better) was to create an extension of Binding of a RandomAccessCollection to itself conform to a RandomAccessCollection, which has bindings as elements, like so:

extension Binding: RandomAccessCollection 
  where Value: RandomAccessCollection & MutableCollection {
  // more code here
}

  // more required extensions to Collection and Sequence here

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

UPDATE

In iOS13 release notes (deprecation section), SwiftUI dropped the conformance of Binding to Collection, and instead offered a workaround, so I'm updating this answer with their suggestion.

The idea is to extend RandomAccessCollection to add a .index() method, which works similarly to .enumerated() by creating a collection of tuples of index and element, but unlike .enumerated() conforms to a RandomAccessCollection, which List and ForEach require.

The usage is:

List(people.indexed(), id: .1.id) { (i, person) in
   HStack() {
      Toggle(person.name, isOn: $people[i].isFavorite)
   }

And the implementation of .indexed() is:

struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
    typealias Index = Base.Index
    typealias Element = (index: Index, element: Base.Element)

    let base: Base

    var startIndex: Index { base.startIndex }

    var endIndex: Index { base.startIndex }

    func index(after i: Index) -> Index {
        base.index(after: i)
    }

    func index(before i: Index) -> Index {
        base.index(before: i)
    }

    func index(_ i: Index, offsetBy distance: Int) -> Index {
        base.index(i, offsetBy: distance)
    }

    subscript(position: Index) -> Element {
        (index: position, element: base[position])
    }
}

extension RandomAccessCollection {
    func indexed() -> IndexedCollection<Self> {
        IndexedCollection(base: self)
    }
}

ORIGINAL Here's what I wanted to achieve:

List($people) { personBinding in 
  HStack() {
      Text(personBinding.wrappedValue.name) 
      Spacer()
      Toggle("", isOn: personBinding.isFavorite)
  }
}

In other words, pass the binding of an array, and get a binding of an element in List's closure.

To achieve that, I created an extension of Binding that makes a Binding of any RandomAccessCollection into a RandomAccessCollection of bindings:

// For all Bindings whose Value is a collection
extension Binding: RandomAccessCollection 
    where Value: RandomAccessCollection & MutableCollection {

  // The Element of this collection is Binding of underlying Value.Element 
  public typealias Element = Binding<Value.Element>
  public typealias Index = Value.Index
  public typealias SubSequence = Self
  public typealias Indices = Value.Indices

  // return a binding to the underlying collection element
  public subscript(position: Index) -> Element {
    get {
      .init(get: { self.wrappedValue[position] },
            set: { self.wrappedValue[position] = $0 })
    }
  }

  // other protocol conformance requirements routed to underlying collection ...

  public func index(before i: Index) -> Index {      
     self.wrappedValue.index(before: i)
  }

  public func index(after i: Index) -> Index {
     self.wrappedValue.index(after: i)
  }

  public var startIndex: Index {
     self.wrappedValue.startIndex
  }

  public var endIndex: Index {
     self.wrappedValue.endIndex
  }
}

This also requires explicit conformance to inherited protocols:

extension Binding: Sequence 
    where Value: RandomAccessCollection & MutableCollection {

  public func makeIterator() -> IndexingIterator<Self> {
    IndexingIterator(_elements: self)
  }
}

extension Binding: Collection 
    where Value: RandomAccessCollection & MutableCollection {

  public var indices: Value.Indices {
    self.wrappedValue.indices
  }
}

extension Binding: BidirectionalCollection 
    where Value: RandomAccessCollection & MutableCollection { 
}

And, if the underlying value is an Identifiable, then it makes the Binding conform to Identifiable too, which removes the need to use id::

extension Binding: Identifiable where Value: Identifiable {
  public var id: Value.ID {
    self.wrappedValue.id
  }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...