Goal
I want to delete an item from a SectionedFetchRequest on a ForEach inside a List. The only solutions I have found are for a regular FetchRequest I have managed to delete it from the UIList but not from the CoreData's ViewContext.
My question is unique because I'm trying to delete from a SectionedFetchRequest which is different than a FetchRequest
#SectionedFetchRequest(entity: Todo.entity(), sectionIdentifier: \.dueDateRelative, sortDescriptors: [NSSortDescriptor(keyPath: \Todo.dueDate, ascending: true)], predicate: nil, animation: Animation.linear)
var sections: SectionedFetchResults<String, Todo>
var body: some View {
NavigationView {
List {
ForEach(sections) { section in
Section(header: Text(section.id.description)) {
ForEach(section) { todo in
TodoRowView(todo: todo)
.frame(maxWidth: .infinity)
.listRowSeparator(.hidden)
}
.onDelete { row in
deleteTodo(section: section.id.description, row: row)
}
}
}
}
func deleteTodo(section: String, row: IndexSet) {
// Need to delete from list and CoreData viewContex.
}
// My old way of deleting notes with a regular fetch Request
func deleteNote(at offsets: IndexSet) {
for index in offsets {
let todo = todos[index]
viewContext.delete(todo)
}
try? viewContext.save()
}
This is how you would use the link...
Add this to the TodoRowView(todo: todo)
.swipeActions(content: {
Button(role: .destructive, action: {
deleteTodo(todo: todo)
}, label: {
Image(systemName: "trash")
})
})
And you need this method in the View
public func deleteTodo(todo: Todo){
viewContext.delete(todo)
do{
try viewContext.save()
} catch{
print(error)
}
}
Or you can use your current setup that uses onDelete on the ForEach
.onDelete { indexSet in
deleteTodo(section: Array(section), offsets: indexSet)
}
That uses this method
func deleteTodo(section: [Todo], offsets: IndexSet) {
for index in offsets {
let todo = section[index]
viewContext.delete(todo)
}
try? viewContext.save()
}
And of course for any of this to work you need a working
#Environment(\.managedObjectContext) var viewContext
At the top of your file
I found this question when searching for a neat solution, couldn't find one so thought I'd share my attempt at deleting from a #SectionedFetchRequest.
var body: some View {
NavigationView {
List {
ForEach(sections) { section in
Section(section.id) {
ForEach(section) { todo in
TodoRowView(todo: todo)
.frame(maxWidth: .infinity)
.listRowSeparator(.hidden)
.onDelete { indexSet in
deleteTodos(section: section, indexSet: indexSet)
}
}
}
}
}
...
private func deleteTodos(section: SectionedFetchResults<String, Todo>.Section, offsets: IndexSet) {
withAnimation {
offsets.map { section[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
Related
I trying to save a Coredata object in a SwiftUI .OnDisappear method and it conflicts with a Date() object in the View. Commenting out the Date() variable in DetailView or commenting out the code in .OnDisappear stops the view from disappearing. I made a minimum reproducible example here from the default New Project with Coredata in Xcode.
Here is a gif of the DetailView disappearing
https://imgur.com/dA2QH4D
ListView
struct ListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: DetailView(item: item), label: {
Text(item.timestamp!, formatter: itemFormatter)
})
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
DetailView
struct DetailView: View {
#State var item: Item
#State private var date = Date() // Comment this
var body: some View {
VStack {
Text(item.timestamp!, formatter: itemFormatter)
NavigationLink(destination: {
EmptyView()
}, label: {
Text("History")
})
}
.onDisappear {
item.timestamp = Date() // Or Comment this
}
}
}
In ListView, add a .navigationViewStyle(.stack) to your NavigationView, such as:
NavigationView {
//...
}.navigationViewStyle(.stack) // <-- here
NavigationView can only have one level of detail NavigationLink i.e. its isDetail property defaults to true. If you want more levels you have to use .isDetail(false) on the 2nd level and beyond.
What is the correct way to delete an entry from a list? Where should the closure be placed?
#ObservedObject var category : Category
var body: some View {
List {
ForEach(category.reminders?.allObjects as! [Reminder]) { reminder in
NavigationLink(destination: ReminderDetail(reminder: reminder)) {
VStack {
Text(reminder.title!)
}
}
}
}
.navigationTitle("Reminders")
VStack {
NavigationLink(destination: AddReminder(category: category)) { Text("Add Reminder") }
}.padding()
}
You can try this:
var body: some View {
List {
ForEach(category.reminders?.allObjects as! [Reminder]) { reminder in
NavigationLink(destination: ReminderDetail(reminder: reminder)) {
VStack {
Text("reminder.title!")
}
}
}.onDelete(perform: self.deleteItem)
}
.navigationTitle("Reminders")
private func deleteItem(at indexSet: IndexSet) {
self.category.reminders(atOffsets: indexSet)
}
I'm trying to edit a List (composed of mutable strings) with an edit button and a function. My code is:
struct annotationsView: View {
#State private var text = ""
// annotations
#State var annotations : [[AnnData]] = [[]]
var body: some View {
VStack{
Form{
HStack{
TextField("Add your annotations here", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Submit") {
annotations[annotations.count - 1].append(AnnData(Anntext: text))
self.hideKeyboard()
text = ""
}
}
List{
ForEach(annotations.indices, id:\.self){index in
ForEach(annotations[index].indices, id:\.self){annotationIndex in
Text(annotations[index][annotationIndex].Anntext)
}.onDelete(perform: self.deleteItem) //<-- Here
}
}
}
}
.background(
Image("Background")
.resizable()
.edgesIgnoringSafeArea(.all)
.navigationBarTitle(Text("Annotations"), displayMode: .inline)
)
.navigationBarItems(trailing: EditButton())
}
private func deleteItem(at indexSet: IndexSet) {
annotations.remove(atOffsets: indexSet)
}
}
struct AnnData : Identifiable{
var id = UUID().uuidString
var Anntext: String
}
Currently, I can not delete a single item; when I delete one, the rest are deleted automatilcally. Also, after that I can't add some items inside the List.
Would you mind explaining what's wrong here?
Thanks for your help!
You need to delete item within a loop because your data is a two-dimensional array.
Like this
List{
ForEach(annotations.indices, id:\.self){index in
ForEach(annotations[index].indices, id:\.self){annotationIndex in
Text(annotations[index][annotationIndex].Anntext)
}
.onDelete { (indexSet) in
annotations[index].remove(atOffsets: indexSet)
}//<-- Here
}
}
I am trying to create a list that only allows users to delete after entering an editing mode. I attempted to try using ternary operation in the onDelete modifier but was unable to figure it out. Any recommendations?
Here is my code:
struct ContentView: View {
#State private var stuff = ["First", "Second", "Third"]
#State private var check = false
var body: some View {
Form {
Button(action: { check.toggle() }, label: { Text(check ? "Editing" : "Edit") })
ForEach(0..<stuff.count) { items in
Section{ Text(stuff[items]) }
}
.onDelete(perform: self.deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
self.stuff.remove(atOffsets: indexSet)
}
}
I assume you look for the following
var body: some View {
Form {
Button(action: { check.toggle() }, label: { Text(check ? "Editing" : "Edit") })
ForEach(0..<stuff.count) { items in
Section{ Text(stuff[items]) }
}
.onDelete(perform: self.deleteItem)
.deleteDisabled(!check) // << this one !!
}
}
struct ContentView: View {
#State private
var stuff = ["First", "Second", "Third"]
var body: some View {
NavigationView {
Form {
ForEach(0..<stuff.count) { item in
Section {
Text(stuff[item])
}
}
.onDelete(
perform: delete)
}
.navigationBarItems(
trailing:
EditButton()
)
.navigationTitle("Test")
}
}
}
extension ContentView {
private func delete(at indexSet: IndexSet) {
stuff.remove(atOffsets: indexSet)
}
}
This can be done directly within the .onDelete method, using the ternary operator:
.onDelete(perform: check ? deleteItem : nil)
I have a filtered list which I need to delete items from using the .onDelete method of ForEach. As the delete(at offsets: IndexSet) function uses the index of the currently displayed list, I need to refer to exactly this order of list items.
Unfortunately I get the following error when I want to refer to the refer to the filteredTools variable:
Cannot use mutating member on immutable value: 'filteredTools' is a get-only property
Any idea how to solve this?
Thanks!
struct ToolList: View {
#EnvironmentObject var userData: UserData
#State var showingFilter = false
#State var editMode = EditMode.inactive
var toolCategories: [String: [Tool]] {
Dictionary(
grouping: userData.tools,
by: { $0.category }
)
}
var filteredTools: [Tool] {
var list = [Tool]()
for category in self.userData.selectedToolCategories {
if category.isSelected && self.toolCategories[category.name] != nil {
list += self.userData.showFavoritesOnly ? self.toolCategories[category.name]!.filter { $0.isFavorite } : self.toolCategories[category.name]!
}
}
return list
}
var body: some View {
NavigationView {
List {
ForEach(filteredTools) { tool in
NavigationLink(destination: ToolDetail(tool: tool).environmentObject(self.userData)) {
ToolRow(tool: tool)
}
}
.onDelete(perform: delete)
}
.navigationBarTitle(Text(NSLocalizedString("Tools", comment: "")))
.navigationBarItems(
leading: EditButton(),
trailing: NavigationBar(showingFilter: self.$showingFilter, editMode: self.$editMode)
.environmentObject(userData)
)
.environment(\.editMode, $editMode)
.sheet(isPresented: $userData.showingProfile) {
ProfileHost()
.environmentObject(self.userData)
}
.sheet(isPresented: $showingFilter) {
FilterView(isPresented: self.$showingFilter, categories: self.$userData.selectedToolCategories)
}
}
}
func delete(at offsets: IndexSet) {
self.filteredTools.remove(atOffsets: offsets) // <-- this is where I get this error message: Cannot use mutating member on immutable value: 'filteredTools' is a get-only property
}
}
I solved it by implementing the delete function back in the model instead of in the view, passing the actually displayed (filtered) List to determine the id of the item to be deleted along with the index of the item:
private func onDelete(offsets: IndexSet) {
userData.delete(from: filteredTools, offsets: offsets)
}