4

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

2 Answers 2

6

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
  }
}
Sign up to request clarification or add additional context in comments.

Comments

0

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

Source: https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-list-or-a-foreach-from-a-binding

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.