2

Until now, to setup bindings between the elements in a dynamic collection and the rows of a List we had to do like that :

List(Array(zip(data.indices, data)), id: \.1.id) { index, _ in
    HStack {
        Text((index + 1).description)
        TextField("", text: Binding(
            get: { data[index].text },
            set: { data[index].text = $0 }
        ))
    }
}

We need : the index of the element for the binding ; + the element identifier for the List (to avoid weird animations) ; and a custom Binding to avoid a crash when deleting the last row.

It's complicated (and I'm not sure it's very efficient). Since WWDC21, we have a new syntax (which can be back-deployed):

List($data) { $item in
    HStack {
        Text("Index ?")
        TextField("", text: $item.text)
    }
}

It's cleaner.

But while it is strongly recommended to use this new syntax, it would be nice to be able to access the element's index in the closure. Do you know how we can do it?

EDIT :

I tried this (it works), but I feel like it's not the right way to do it :

let d = Binding(get: {
    Array(data.enumerated())
}, set: {
    data = $0.map {$0.1}
})
List(d, id: \.1.id) { $item in
    HStack {
        Text("\(item.0 + 1)")
        TextField("", text: $item.1.text)
    }
}
Adrien
  • 1,579
  • 6
  • 25

2 Answers2

4

You can build wrapper by yourself:

struct ListIndexed<Content: View>: View {
    let list: List<Never, Content>
    
    init<Data: MutableCollection&RandomAccessCollection, RowContent: View>(
        _ data: Binding<Data>,
        @ViewBuilder rowContent: @escaping (Data.Index, Binding<Data.Element>) -> RowContent
    ) where Content == ForEach<[(Data.Index, Data.Element)], Data.Element.ID, RowContent>,
    Data.Element : Identifiable,
    Data.Index : Hashable
    {
        list = List {
            ForEach(
                Array(zip(data.wrappedValue.indices, data.wrappedValue)),
                id: \.1.id
            ) { i, _ in
                rowContent(i, Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }))
            }
        }
    }
    
    var body: some View {
        list
    }
}

Usage:

ListIndexed($items) { i, $item in
    HStack {
        Text("Index \(i)")
        TextField("", text: $item.text)
    }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • Thank you @Philip. It's a good idea. But at the moment, with your wrapper the app crashes if you try to delete the last row. – Adrien Aug 20 '21 at 12:18
  • You could replace `data[i]` by `Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }).projectedValue` to resolve the problem. But I'm not sure if this is the clearest way to do it and how it impacts performance. – Adrien Aug 20 '21 at 12:25
  • @Adrien oh, so my wrapper gets uglier :D I don't think this will cause dramatic performance problems, reading from array is O(1). I think that source code of new `List($data)` should have something similar too=) Also why you you call `projectedValue`? AFAIK it's only needed for "$" to work, works fine with just init to me – Phil Dukhov Aug 20 '21 at 13:43
  • You are right about `projectedValue`. You're also right about how ugly this edit is. But I don't know of any other way to avoid this crash with an out-of-bounds error than to create these custom binding instances. – Adrien Aug 20 '21 at 14:19
  • It's brilliant. And it will be very useful, – Adrien Aug 20 '21 at 14:44
  • @Adrien tnx for your tips. I am using the new binding syntax and deleting the `item` from the array by its `index` using `remove(at: index)` method but I am still getting array out of bounds error. I made sure the index that I am passing is correct and well within bounds. So, is there anything that I am missing here? I thought the new binding syntax was supposed to solve this issue. – Ram Patra Jan 03 '23 at 22:30
  • 1
    Actually, I found the issue and it's a weird one. I had the code for removing the item inside an `animation` block and this was causing the error. Getting rid of the animation block got rid of the array out-of-bounds error. – Ram Patra Jan 03 '23 at 23:04
0

If your items are Equatable, you can use firstIndex(of:) to get their index:

List($data) { $item in
    HStack {
        Text("Index \(data.firstIndex(of: item)! + 1)")
        TextField("", text: $item.text)
    }
}
Adam
  • 4,405
  • 16
  • 23
  • Items don't need to be Equatable, we can use `firstIndex (where: {$0.id == id})` But the complexity of `firstIndex` is O(n) (n = length), so O(n ^ 2) for the whole ForEach. We may be able to find better. – Adrien Aug 19 '21 at 17:37