How to Generate a Binding for Each Array Element

How to generate a binding for each array element

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

Using ForEach with a an array of Bindings (SwiftUI)

Trying a different approach. The FormField maintains it's own internal state and publishes (via completion) when its text is committed:

struct FormField : View {
@State private var output: String = ""
let viewModel: FormFieldViewModel
var didUpdateText: (String) -> ()

var body: some View {
VStack {
TextField($output, placeholder: Text(viewModel.placeholder), onCommit: {
self.didUpdateText(self.output)
})

Line(color: Color.lightGray)
}.padding()
}
}
ForEach(viewModel.viewModels) { vm in
FormField(viewModel: vm) { (output) in
vm.output = output
}
}

Two way binding on Array elements using form

I completely suspect that this behavior is happening because of conflict in the name attribute value. For this case only, if you splice the newItem at first location, it only adds that's variable and other DOM's doesn't re-render. For cross verification you can try replacing input element with simple binding like {{classData.title}} and everything works fine.

This behavior can easily be solved by not conflicting name attribute value for all time. What that means is to assign a unique id variable with each collection item and use it.

this.classesData = [
{ id: 1, title: 'Hello0' },
{ id: 2, title: 'Hello1' },
{ id: 3, title: 'Hello2' }
];
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
newData.id = Date.now()
this.classesData.splice(1, 0, newData);
}

Template

<div *ngFor="let classData of classesData;let i=index">
<input [(ngModel)]="classData.title" [name]="'title_'+classData.id" type="text">
</div>

Stackblitz


You can also verify the same by removing name attribute from each input field. But that would not suffice, it would throw

ERROR Error: If ngModel is used within a form tag, either the name
attribute must be set or the form control must be defined as
'standalone' in ngModelOptions.

So add [ngModelOptions]="{standalone: true}" on each input field to make input working without name attribute. As suggested in another answer by @briosheje, you can also re-enforce rendering using trackBy.

PS: I'm investigating why this works differently when there is a combination of name and input, I suspect about form API wiring with input element. I'll update the answer as soon as I get something.

How do I bind the parameter 'this' to each element of an array when using forEach()?

The .forEach() facility passes three arguments to your callback function:

  1. The current element of the array;
  2. The index into the array;
  3. The array itself.

Thus:

collectibles.forEach(function(collectible) {
// the parameter "collectible" will be the
// current array element
});

There's really no need to bind this, but if you absolutely wanted to you could:

collectibles.forEach(function callback(collectible) {
if (this !== collectible)
callback.call(collectible, collectible);
else {
// body of function
}
});

Using a protocol array with ForEach and bindable syntax

This is a thorny problem with your data types.

If you can change your data types, you can make this easier to solve.

For example, maybe you can model your data like this instead, using an enum instead of a protocol to represent the variants:

struct Testable {
let id: UUID
var name: String
var variant: Variant

enum Variant {
case animal(Animal)
case human(Human)
}

struct Animal {
var owner: String
}

struct Human {
var age: Int
}
}

It will also help to add accessors for the two variants' associated data:

extension Testable {
var animal: Animal? {
get {
guard case .animal(let animal) = variant else { return nil }
return animal
}
set {
guard let newValue = newValue, case .animal(_) = variant else { return }
variant = .animal(newValue)
}
}

var human: Human? {
get {
guard case .human(let human) = variant else { return nil }
return human
}
set {
guard let newValue = newValue, case .human(_) = variant else { return }
variant = .human(newValue)
}
}
}

Then you can write your view like this:

class ContentViewModel: ObservableObject {
@Published var testables: [Testable] = []
}

struct ContentView: View {
@StateObject var vm: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
List {
ForEach($vm.testables, id: \.id) { $testable in
VStack {
TextField("Name", text: $testable.name)

if let human = Binding($testable.human) {
Stepper("Age: \(human.wrappedValue.age)", value: human.age)
}

else if let animal = Binding($testable.animal) {
HStack {
Text("Owner: ")
TextField("Owner", text: animal.owner)
}
}
}
}
}

HStack {
Button("Add animal") {
vm.testables.append(Testable(
id: UUID(),
name: "Mick",
variant: .animal(.init(owner: "harry"))
))
}
Button("Add Human") {
vm.testables.append(Testable(
id: UUID(),
name: "Ash",
variant: .human(.init(age: 26))
))
}
}
}
}
}

Binding in a ForEach in SwiftUI

Assuming your elements is state of items array, it can be

List {
ForEach(elements.indices, id: \.self) { i in
CheckBoxView(checked: $elements[i].checked)
}
}

Bind each array element to option tag

You can't bind a object to the [value] property of the option element.

I would bind the object's id to the [value] property of the option element and use the (change) event of the select.

See the example below:

import { Component } from '@angular/core';

@Component({
selector: 'my-app',
template: `
Select your favorite serie:

<select (change)="onChange($event.target.value)">
<option *ngFor="let serie of series" [value]="serie.id">{{ serie.name }}</option>
</select>

<br />
<br />

Selected Serie: <br />
<span *ngIf="selectedSerie">
{{ selectedSerie.id }} - {{ selectedSerie.name }}
</span>
`
})
export class AppComponent {

private series: any[];
private selectedSerie: any;

constructor() {
this.series = [
{ id: 1, name: 'Friends' },
{ id: 2, name: 'How I met Your Mother' },
{ id: 3, name: 'Modenr Family' }
];

this.selectedSerie = this.series[0];
}

onChange(serieId: any): void {
this.selectedSerie = this.series.find(serie => serie.id == serieId);
}
}

See the complete example here: http://plnkr.co/edit/OOYx3LiN1pO3qffKn7lq

@Binding and ForEach in SwiftUI

You can use something like the code below. Note that you will get a deprecated warning, but to address that, check this other answer: https://stackoverflow.com/a/57333200/7786555

import SwiftUI

struct ContentView: View {
@State private var boolArr = [false, false, true, true, false]

var body: some View {
List {
ForEach(boolArr.indices) { idx in
Toggle(isOn: self.$boolArr[idx]) {
Text("boolVar = \(self.boolArr[idx] ? "ON":"OFF")")
}
}
}
}
}


Related Topics



Leave a reply



Submit