SwiftUI: extension on array to get a Binding on an instance of the array

Viewed 330

I'd like to have an extension on Array<T> that basically returns a Binding<T>. The purpose of this would be to have a convenient way to create a Binding in DetailView that was called from a NavigationLink of a List.

Here's what I got so far:

extension Array where Element: Identifiable {
    mutating func getBinding(of instance: Element) -> Binding<Element> {
        if let index = self.firstIndex(where: { $0.id == instance.id }) {
            return Binding(
                get: { self[index] }, //error
                set: { self[index] = $0}) //error
        } else {
            fatalError() //implement error handling here
        }
    }
}

I am getting the error Escaping closure captures mutating 'self' parameter at the specified places. How can I work around this?

TL;DR

Here's how I'd like to use this extension:

class ViewModel: ObservableObject {
    @Published var items: [Item]
    
    init(with items: [Item] = [Item]()) {
        self.items = items
    }
}

struct Item: Identifiable, Hashable {
    let id: UUID
    var title: String
    
    static var sampleItems: [Item] {
        var items = [Item]()
        for i in 0..<10 {
            items.append(Item(id: UUID(), title: "item \(i)"))
        }
        return items
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel(with: Item.sampleItems)
    
    var body: some View {
        NavigationView {
            List {
                Section {
                    ForEach(viewModel.items) { item in
                        //MARK: Using Array.getBinding(of:) here
                        NavigationLink(item.title, destination: DetailView(item: viewModel.items.getBinding(of: item)))
                    }
                }
            }
        }
    }
}

struct DetailView: View {
    @Binding var item: Item
    
    var body: some View {
        TextField("Item Title", text: $item.title)
    }
}
1 Answers

Change the extension to:

extension Binding {
    func getBinding<T>(of instance: T) -> Binding<T>
    where Value == [T], T: Identifiable {
        if let index = self.wrappedValue.firstIndex(where: { $0.id == instance.id }) {
            return .init(
                get: { self.wrappedValue[index] }, //error
                set: { self.wrappedValue[index] = $0 }) //error
        } else {
            fatalError() //implement error handling here
        }
    }
}

now instead of DetailView(item: viewModel.items.getBinding(of: item)) do DetailView(item: $viewModel.items.getBinding(of: item)) (note the $)

EDIT, Bonus:

I've got a bonus for you, hopefully you'll like it. This will make the process much nicer and the code much cleaner. Note that it has 0 difference with your current code, performance-wise.
Add this extension to begin:

extension Binding {
    subscript<T>(_ index: Int) -> Binding<T> where Value == [T] {
        .init(get: {
            self.wrappedValue[index]
        },
        set: {
            self.wrappedValue[index] = $0
        })
    }
}

and change your ForEach to this:

ForEach(viewModel.items.indices) { index in
    let item = viewModel.items[index]
    NavigationLink(item.title, destination: DetailView(item: $viewModel.items[index]))
}

hopefully you like it :)

Related