swiftswiftuiswiftdataswiftui-foreach

Problem with binding a SwiftData model property to a Toggle


I'm trying to display a checklist in SwiftUI which is stored in SwiftData:

@Model
class ChecklistItem: Identifiable, ObservableObject {
    let id = UUID()
    var title: String
    var isChecked: Bool
    var sortOrder: Int
    
    init(title: String, isChecked: Bool, sortOrder: Int) {
        self.title = title
        self.isChecked = isChecked
        self.sortOrder = sortOrder
    }
}

@Model
class ChecklistModel: Identifiable, Hashable, ObservableObject {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    static func == (lhs: ChecklistModel, rhs: ChecklistModel) -> Bool {
        return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
    }
    
    let id = UUID()
    var title: String
    @Relationship var items: [ChecklistItem]
    
    init(title: String, items: [ChecklistItem]) {
        self.title = title
        self.items = items
    }
}

ScrollView {
    ForEach(checklist.items.sorted(by: { $0.sortOrder < $1.sortOrder }), id: \.self) { item in

    //ForEach(checklist.items.sorted(by: {$0.sortOrder < $1.sortOrder}).indices, id: \.self) { index in
    //  let item = checklist.items[index]
    // if I do it this way, the items are not sorted in the correct order

    HStack {
        Toggle(item.title, isOn: item.$isChecked)
            .toggleStyle(CheckboxToggleStyle())
        Text("\(item.sortOrder)")
    }
}

But if I try it the uncommented way so that the sorting works correctly, I get this error:

Initializer 'init(_:isOn:)' requires that 'Binding<Subject>' conform to 'StringProtocol'

So what's the solution here? How can I sort these items correctly in the view and also bind the Toggle value to the model?


Solution

  • Assuming checklist is one ChecklistModel, try this approach using a @Bindable as shown in the example code, together with the updated @Model class without the ObservableObject.

    Note, @Model macro makes the class conform to Observable, Hashable and Identifiable.

     ScrollView {
         ForEach(checklist.items.sorted(by: { $0.sortOrder < $1.sortOrder })) { item in
             @Bindable var item = item  // <-- here
             HStack {
                 Toggle(item.title, isOn: $item.isChecked)  // <-- here
                     .toggleStyle(.automatic)
                 Text("\(item.sortOrder)")
             }
         }
     }
     
     @Model
     class ChecklistItem {  // <-- here
         var title: String
         var isChecked: Bool
         var sortOrder: Int
         
         init(title: String, isChecked: Bool, sortOrder: Int) {
             self.title = title
             self.isChecked = isChecked
             self.sortOrder = sortOrder
         }
     }
    
     @Model
     class ChecklistModel {  // <-- here
         var title: String
         
         @Relationship var items: [ChecklistItem]
         
         init(title: String, items: [ChecklistItem]) {
             self.title = title
             self.items = items
         }
     }
     
    

    See this link SwiftData with model, it gives you some good examples of how to use SwiftData with @Model to persist data in your app.