Reputation: 49580
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
Upvotes: 3
Views: 1833
Reputation: 1
This is the simplest solution:
struct User: Identifiable {
let id = UUID()
var name: String
var isContacted = false
}
struct ContentView: View {
@State private var users = [
User(name: "Taylor"),
User(name: "Justin"),
User(name: "Adele")
]
var body: some View {
List($users) { $user in
Text(user.name)
Spacer()
Toggle("User has been contacted", isOn: $user.isContacted)
.labelsHidden()
}
}
}
Upvotes: 0
Reputation: 49580
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
}
}
Upvotes: 6