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:
- The current element of the array;
- The index into the array;
- 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
How Are Hash Collisions Handled
Swift Combine How to Skip an Event
Swift: Using "/" Slash in Filename with Createdirectoryatpath
How to Find the Operator Definition in Swift
What Happens When You Run Deprecated Code in Swift
How to Assign an Array to a Class Property by Reference Rather Than a Copy
Any Way to Chain == and || Operands
How to Disable a Constraint Programmatically
Swift Struct Initialization, Making Another Struct Like String
How to Draw a Line Between Two Points in Scenekit
Hot to Decode JSON Data That Could and Array or a Single Element in Swift
How to Save the Attributed String (Text) into File (Swift, Cocoa)
How to Update a Value in a Nested Dictionary Given Path Fragment in Swift
Using Delegates on Generic Protocol