Delete Core Data Row When Long Pressing - swiftui

I have created a new Mac OS xcode application with core data and was playing around with the default code that generates that allows you to add items.
But I wanted to change the way in which the rows are deleted. With using a long press gesture to trigger the delete.
View:
var body: some View {
List {
ForEach(items) { item in
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
//.onDelete(perform: deleteItems) Want to change this command to the one below
.onLongPressGesture(minimumDuration: 1.5, perform: deleteItems)
}
.toolbar {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Delete Item Function
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)")
}
}
But I get the following error
Cannot convert value of type '(IndexSet) -> ()' to expected argument type '() -> Void'

".onDelete(perform action: ((IndexSet) -> Void)?)" and
".onLongPressGesture(minimumDuration: Double = 1.5, perform action: #escaping () -> Void)"
as you can see have different signatures. So what you are doing is not working.
You could try something like this:
List {
ForEach(items, id: \.self) { item in
Text("Item at \(item)")
.onLongPressGesture(minimumDuration: 1.5, perform: {deleteItems2(item)})
}
// .onDelete(perform: deleteItems)
}
private func deleteItems2(_ item: Item) {
if let ndx = items.firstIndex(of: item) {
viewContext.delete(items[ndx])
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

SwiftUI - Delete from Sectioned Dynamic List [duplicate]

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)")
}
}
}

How to solve this async/await error (CoreData)?

I am new to Core Data and I'm trying to create a notes app.My project builds but when I try to add a note it crashes. I'm a newbie to using async/await, and there's not many explanations online, so I don't understand the compiler error I have.
It says "No 'async' operations occur within 'await' expression".
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Note.name, ascending: true)])
var notes: FetchedResults<Note>
#State private var NoteIds: Set<Note.ID> = []
var body: some View {
List(notes, selection: $NoteId){ note in
Text(note.name)
}
.toolbar{
ToolbarItem(placement: .primaryAction){
Button(action: newNote){
Label("New Note", systemImage: "square.and.pencil")
}
}
}
}
private func newNote(){
Task{ await newNote(name: "New Note", text:"")}
}
private func newNote(name: String, text: String) async {
await viewContext.perform { //where error is
let note = Note(context: viewContext)
note.id = UUID()
note.name = name
note.text = text
}
try? PersistenceController.shared.saveContext()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
PersistanceController:
final class PersistenceController{
static let shared = PersistenceController()
lazy var container: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Notes")
container.loadPersistentStores {description, error in
if let error = error {
fatalError("Unable to load persistent stores: \(error)")
}
}
return container
}()
private init() {}
public func saveContext(backgroundContext: NSManagedObjectContext? = nil) throws {
let context = backgroundContext ?? container.viewContext
guard context.hasChanges else {return}
try context.save()
}
}
Make sure your projects only supports
iOS 15.0+
iPadOS 15.0+
macOS 12.0+
Mac Catalyst 15.0+
tvOS 15.0+
watchOS 8.0+
Xcode 13.0+
I want to emphasize the versions async/await is Swift and is supported by more platform versions than the Core Data method.
With a basic project provided by Xcode this works in the ContentView
private func addItem() async{
do {
try await viewContext.perform {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
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)")
}
}
https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/3802018-perform
From WWDC Bring Core Data Concurrency to Swift and SwiftUI
The view context is on the main thread you don't need async/await. Here is the sample addItem from Xcode's Core Data app template:
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)")
}
}
}

IOS detail view disappearing when contains Date object and tries to update Coredata

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.

Why my list doesn't scroll when I scroll down inside the list?

Why my list doesn't scroll when I scroll down inside the list?
I know the question sounds silly but I cannot make my list scroll, it SHOULD SCROLL, but doesn't....
My list is very long and I can scroll vertically if my finger scrolls down outside the list but NOT inside (touching the list elements)
I know I can use foreach and dividert to simulate a list but I want to keep using the ondelete etc methods part of the list.
update: the root cause is because I'm using onTapgesture etc, but how can I keep them and still have a list that scrolls?
List {
ForEach(fetchRequest, id: \.self) { task in
Text(task.desc!)
.foregroundColor(task.done ? .white : task.today ? .white : .black)
.listRowBackground(task.done ? Color.green : task.today ? Color.blue : Color.white)
.onTapGesture {
//isPresented.toggle()
self.taskToEdit = task
}
.swipeActions(allowsFullSwipe: true) {
Button(role: .cancel) {
task.done.toggle()
task.today = false
task.date_completion = Date()
do {try viewContext.save()} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
print("mark as done")
} label: {
Label("Done", systemImage: "checkmark")
}
.tint(.green)
}
.simultaneousGesture(LongPressGesture()
.onEnded { _ in
let impactMed = UIImpactFeedbackGenerator(style: .light)
impactMed.impactOccurred()
task.today.toggle()
do {try viewContext.save()} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
print("Loooong")
print(task.today)
}
)
}
.onDelete(perform: deleteItems)
}
//.listRowSeparator(.hidden)
//.listRowInsets(.init(top: 4, leading: 8, bottom: 4, trailing: 8))
.sheet(item: self.$taskToEdit, content: { taskToEdit in
itemDetail(taskItem: taskToEdit, textFieldText: taskToEdit.detail ?? "")
})
}

onDelete is attached to ForEach but there's no swipe gestures

With .onDelete() attached to the ForEach, nothing happens - cannot swipe to delete nor does tapping Edit show anything. Not sure what I've done wrong. It compiles fine in simulator and on my iPhone (iOS 14.5) Here's my actual code.
NavigationView {
List {
ForEach(medicines) { medicine in
HStack {
VStack(alignment: .leading) {
Text("\(medicine.name!) \(medicine.strength) \(medicine.unit!)" as String)
.font(.headline)
Text("\(medicine.quantity) x \(medicine.form!) \(medicine.direction!)" as String)
.font(.subheadline)
}
}
.frame(height: 50)
}
.onDelete(perform: deleteMedicines)
.listStyle(PlainListStyle())
.navigationBarTitle("Medicines")
.navigationBarItems(leading: EditButton(), trailing: Button(action: {
self.showingAddScreen.toggle()
}) {
Image(systemName: "plus")
})
.sheet(isPresented: $showingAddScreen) {
AddMedicineView().environment(\.managedObjectContext, self.viewContext)
}
}
}
Then here's the deleteMedicines() function:
private func deleteMedicines(offsets: IndexSet) {
withAnimation {
offsets.map { medicines[$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)")
}
}
}
I've seen a few different ways of doing this and I've tried a few. I had it working when I was first playing around with List. The deleteMedicines code is borrowed from Xcode template for Core Data.
Provided code is not testable, but try to keep only onDelete modifier on ForEach, everything else move below onto List (placement of modifiers are important and can be a reason of your issue), like
NavigationView {
List {
ForEach(medicines) { medicine in
// content here
}
.onDelete(perform: deleteMedicines) // << here !!
}
.listStyle(PlainListStyle())
.navigationBarTitle("Medicines")
.navigationBarItems(leading: EditButton(), trailing: Button(action: {
self.showingAddScreen.toggle()
}) {
Image(systemName: "plus")
})
.sheet(isPresented: $showingAddScreen) {
AddMedicineView().environment(\.managedObjectContext, self.viewContext)
}