3

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 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

2 Comments

Swift is a type inferred language return .init(
@leo i tried to make the least amount of chanegs i can, otherwise i could do some other optimizations as well. That being said, you are right and .init works better, so i changed the code. thanks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.