Search code examples
swiftuiswiftdata

SwiftData - Delete List Item When Cell View Clicked from Different Struct


I am trying to build To-Do app such as Microsoft To-Do App.

enter image description here

There is Row Cell View(Checkbox and Text horizontally) and Custom Checkbox Toggle Style View for the List.

I want to delete Item of clicked Cell View but I couldn't organize "sharing Index"

Content View:

struct ContentView: View {
  @Environment(\.modelContext) private var context
  @Query(sort: \Note.title) var noteList: [Note]

  var body: some View {
    List {                  
      ForEach(noteList, id: \.id) { item in
        NoteCell(myNote: Note(title: item.title, desc: item.desc))
          .sheet(isPresented: $showEditNoteSheet, content: {
            EditNoteSheet(noteTitle: item.title, noteDesc: item.desc)
          })
      }
      .onDelete(perform: { indexSet in
        for index in indexSet {
          deleteItem(index: index)
        }
      })
    }
  }
 
  func deleteItem(index: Int){
    context.delete(noteList[index])
  }
}

Note Cell View:

struct NoteCell: View {
  @Environment(\.modelContext) private var modelContext
  var myNote: Note
    
  @State var isChecked = false
  @State var noteSheet = false
    
  var body: some View {
    Button {
      noteSheet.toggle()
      //Delete action here?
    } label: {
      HStack {
        Toggle(isOn: $isChecked) {
        }
        .toggleStyle(iOSCheckboxToggleStyle())
                 
        Text(myNote.title)
      }
      .foregroundStyle(.black)
    }
    .sheet(isPresented: $noteSheet, content: {
      AddNoteSheet(noteTitle: myNote.title, noteDesc: myNote.desc)
    })
  }
}

CustmIOSCheckBoxToggleStyle:

struct iOSCheckboxToggleStyle: ToggleStyle {
  func makeBody(configuration: Configuration) -> some View {
    // 1
    Button(action: {        
      // 2
      configuration.isOn.toggle()
      // Delete action here?
    }, label: {
      HStack {
        // 3
        Image(systemName: configuration.isOn ? "checkmark.square" : "square")
        configuration.label
      }
    })
  }
}

Solution

  • If I understand your question correctly, you want to remove items that have been checked?

    If so, there is a way for SwiftData to do that for you. Assuming that each Note has an isChecked property, you can add a filter so that only unchecked properties are listed:

    @Query(
      filter: #Predicate { $0.isChecked == false },
      sort: \Note.title
    ) var noteList: [Note]
    

    Then in your NoteCellView, the quickest way is to use the Note's `isChecked property in your action:

    struct NoteCellView: View {
      // mark your note as Bindable
      @Bindable var note: Note
    
      var body: some View {
        // ...rest of view omitted...
        Toggle(isOn: $note.isChecked) {
        // ...etc...
      }
    }
    

    When you check the toggle, the note object updates, and SwiftData should automatically remove it from the Query collection - and that in turns removes the note from the list.

    You may want a bit more of a sophisticated UI – for example, in reminders apps it's not uncommon to delay the removal for a while, giving the user a chance to uncheck the box if they checked it in error. In that case, you may want to keep the isChecked state on the toggle, and use some form of cancelable task that is triggered to run after a certain amount of time (say, 2 seconds). After that delayed start, you'd set note.isChecked = true in code. If the user unchecks the box in that task, you cancel the task so the underlying Note isn't marked as complete after all.

    In this option, you don't need to make Note @Bindable because you're only going to change it through code, not by binding it to a view. This would make your NoteCellView look something like this:

    struct NoteCell: View {
      @Environment(\.modelContext) private var modelContext
      var myNote: Note
        
      @State var isChecked = false
      @State var noteSheet = false
      @State var markNoteCompleteTask: Task<Void, Error>?
    
      var body: some View {
        Button {
          noteSheet.toggle()
          //Delete action here?
        } label: {
          HStack {
            Toggle(isOn: $isChecked) {
            }
            .toggleStyle(iOSCheckboxToggleStyle())
                     
            Text(myNote.title)
          }
          .foregroundStyle(.black)
        }
        .sheet(isPresented: $noteSheet, content: {
          AddNoteSheet(noteTitle: myNote.title, noteDesc: myNote.desc)
        })
        .onChange(of: isChecked) {
          if isChecked {
            markNotCompleteTask = Task { @MainActor in
              try await Task.sleep(for: .seconds(2))
              try await Task.checkCancellation() // don't continue if the task was previously cancelled
              withAnimation {
                note.isChecked = true
              }
            }
          } else {
            // if isChecked is now false, cancel the task
            markNoteCompleteTask?.cancel()
          }
        }
      }
    }
    

    As long as the @Query in ContentView still filters to show only notes that are not checked, after 2 seconds the note will smoothly disappear.