Error: Initializer 'init(_:)' requires that 'Binding<String>' conform to 'StringProtocol'

user1366265 picture user1366265 · Sep 23, 2019 · Viewed 13.7k times · Source

I am getting the above error and couldn't figure out how to solve it. I have an array of objects that contain a boolean value, and need to show a toggle for each of these boolean.

Below is the code.

class Item: Identifiable {
    var id: String
    var label: String
    var isOn: Bool
}

class Service: ObservableObject {
    var didChange = PassthroughSubject<Void, Never>()

    var items: [Item] {
        didSet {
            didChange.send(())
        }
    }
}

struct MyView: View {
    @ObservedObject var service: Service

    var body: some View {
        List {
            ForEach(service.items, id: \.self) { (item: Binding<Item>) in
                Section(header: Text(item.label)) {  // Error: Initializer 'init(_:)' requires that 'Binding<String>' conform to 'StringProtocol'
                    Toggle(isOn: item.isOn) {
                        Text("isOn")
                    }
                }
            }
        }
        .listStyle(GroupedListStyle())
    }
}

Answer

John M. picture John M. · Sep 24, 2019

Use the @Published property wrapper in your Service class, rather than didChange, and iterate over the indices of service.items like so:

struct Item: Identifiable {
    var id: String
    var label: String
    var isOn: Bool {
        didSet {
            // Added to show that state is being modified
            print("\(label) just toggled")
        }
    }
}

class Service: ObservableObject {
    @Published var items: [Item]

    init() {
        self.items = [
            Item(id: "0", label: "Zero", isOn: false),
            Item(id: "1", label: "One", isOn: true),
            Item(id: "2", label: "Two", isOn: false)
        ]
    }
}

struct MyView: View {
    @ObservedObject var service: Service

    var body: some View {
        List {
            ForEach(service.items.indices, id: \.self) { index in
                Section(header: Text(self.service.items[index].label)) {
                    Toggle(isOn: self.$service.items[index].isOn) {
                        Text("isOn")
                    }
                }
            }
        }
        .listStyle(GroupedListStyle())
    }
}

Update: Why use indices?

In this example, we need to get two things from each Item in the model:

  1. The String value of the label property, to use in a Text view.
  2. A Binding<Bool> from the isOn property, to use in a Toggle view.

(See this answer where I explain Binding.)

We could get the label value by iterating over the items directly:

ForEach(service.items) { (item: Item) in
    Section(header: Text(item.label)) {
    ...
}

But the Item struct does not contain a binding. If you tried to reference Toggle(isOn: item.$isOn), you'd get an error: "Value of type 'Item' has no member '$isOn'."

Instead, the Binding is provided at the top level by the @ObservedObject property wrapper, meaning the $ has to come before service. But if we're starting from service, we'll need an index (and we cannot declare intermediate variables inside the ForEach struct, so we'll have to compute it inline):

ForEach(service.items) { (item: Item) in
    Section(header: Text(item.label)) {
        Toggle(isOn: self.$service.items[self.service.items.firstIndex(of: item)!].isOn) {
        // This computes the index       ^--------------------------------------^
            Text("isOn")
        }
    }
}

Oh, and that comparison to find the index would mean Item has to conform to Equatable. And, most importantly, because we are looping over all items in the ForEach, and then again in the .firstIndex(of:), we have transformed our code from O(n) complexity to O(n^2), meaning it will run much more slowly when we have a large number of Items in the array.

So we just use the indices. Just for good measure,

ForEach(service.items.indices, id: \.self) { index in

is equivalent to

ForEach(0..<service.items.count, id: \.self) { index in