Как создать привязку для каждого элемента массива - PullRequest
0 голосов
/ 30 мая 2020

Если бы у меня была переменная @State или @ObservedObject со свойством массива, и я хотел бы использовать List и передать привязку каждого элемента массива в некоторый дочерний View (например, Toggle или TextField), есть ли стандартный способ сделать это?

Упрощенный пример:

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

Это кажется довольно распространенным сценарием, но я не могу найти очевидное решение в стороне от ручного создания отдельного массива привязок.

Единственное элегантное решение, которое я придумал (добавлю его в качестве ответа, если нет чего-то лучшего), было создание расширения Binding из RandomAccessCollection к самому себе соответствует RandomAccessCollection, который имеет привязки как элементы, например:

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

  // more required extensions to Collection and Sequence here

1 Ответ

2 голосов
/ 30 мая 2020

ОБНОВЛЕНИЕ

В примечаниях к выпуску iOS13 (раздел об устаревании) SwiftUI отказался от соответствия Binding до Collection и вместо этого предложил обходной путь, поэтому я обновляю этот ответ их предложением.

Идея состоит в том, чтобы расширить RandomAccessCollection, чтобы добавить метод .index(), который работает аналогично .enumerated(), создавая коллекцию кортежей индекса и элемента , но в отличие от .enumerated() соответствует RandomAccessCollection, которое требует List и ForEach.

Используется:

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

И реализация .indexed() :

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

ОРИГИНАЛ Вот чего я хотел достичь:

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

Другими словами, передайте привязку массива и получите привязка элемента в закрытии List.

Для этого я создал расширение Binding, которое превращает Binding любого RandomAccessCollection в RandomAccessCollection привязок:

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

Это также требует явного соответствия унаследованным протоколам:

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

И, если базовое значение является Identifiable, тогда привязка также будет соответствовать Identifiable, что устраняет необходимость использовать id::

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