I have some items in a list which I am adding swipe to delete functionality to. When using a delete function, I'm getting an error telling me that the FetchedResults<tem> object has no member 'remove'. What's happening?
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Item.entity(), sortDescriptors:[]) var items: FetchedResults<Item>
...
List {
ForEach(items, id: \.self) { (item: Item) in
Text(item.title ?? "New Item")
.font(.headline)
}
.onDelete(perform: deleteItems)
}
func deleteItems(at offsets: IndexSet) {
self.items.remove(atOffsets: offsets)
}
Use delete method on managedObjectContext. Also, don't forget to save once deletion is complete.
func deleteItems(at offsets: IndexSet) {
for index in offsets {
let item = items[index]
moc.delete(item)
}
do {
try moc.save()
} catch {
// handle the Core Data error
}
}
Related
I have a simple list that contains a textfield for each word object (just a string with an ID)
When I try to move rows in my app, XCode prints this warning in the console:
"ForEach<Binding<Array>, UUID, TextField>: the ID D5F23F6F-82BB-43AD-9ECF-DEA1B56AB345 occurs multiple times within the collection, this will give undefined results!"
Why is this so?
No warning occurs if I use Text instead of TextField.
This is my word struct:
struct Word: Identifiable, Hashable {
let id: UUID = UUID()
var str: String
init(_ str: String) {
self.str = str
}
}
And my list looks like this:
struct ReorderTest: View {
#State private var items = [Word("Chicken"), Word("Pork"), Word("Beef")]
var body: some View {
NavigationView {
List {
// USING TEXTFIELD LEADS TO WARNINGS
ForEach($items, id: \.self.id) { $item in
TextField("item", text: $item.str)
}
// USING TEXT DOES NOT LEAD TO WARNINGS
// ForEach(items, id: \.self.id) { item in
// Text(item.str)
// }
.onMove(perform: move)
.onDelete(perform: delete)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
}
}
func delete(at offsets: IndexSet) {
self.items.remove(atOffsets: offsets)
}
func move(from source: IndexSet, to destination: Int) {
self.items.move(fromOffsets: source, toOffset: destination)
}
}
try this, works for me:
func move(from source: IndexSet, to destination: Int) {
DispatchQueue.main.async {
self.items.move(fromOffsets: source, toOffset: destination)
}
}
Note, you could simply use ForEach($items), since Word is Identifiable. Why does this happens, I have no idea, we do not have access to the code for the move.
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 do I save the like state for each individual cell? I decided to save via CoreData, but the like is saved for all cells at once.
In the Core Data model (LikedDB) there is an attribute such as isLiked
In the class there is a variable isLiked, which changes the state of the like:
class ModelsViewModel: ObservableObject{
#Published var isLiked = false
func like() {
isLiked.toggle()
}
}
This is how I save the like state from ModelsViewModel
And in label I use
struct CellView: View{
//For CoreData
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: LikedDBE.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \LikedDBE.name, ascending: true)]) var manyLikedDB: FetchedResults<LikedDBE>
//For like
#ObservedObject var cellViewModel: ModelsViewModel = ModelsViewModel()
var body: some View{
Button(action: {
let likedDBE = LikedDBE(context: self.managedObjectContext)
likedDBE.isLiked = cellViewModel.isLiked //I indicate that isLiked from CoreData = isLiked from ModelsViewModel()
do{
cellViewModel.like() //func from ModelsViewModel()
try self.managedObjectContext.save() //Save
} catch{
print(error)
}
}, label: {
Image(systemName: cellViewModel.isLiked ? "heart.fill" : "heart") //Here I use
.frame(width: 22, height: 22)
.foregroundColor(cellViewModel.isLiked ? .red : .black) //And here I use
})
And if I use cellViewModel.isLiked, then when I click like, the like is displayed only on the one I clicked on, but the state is not saved when restarting the application, if I use likedDB.isLiked, then the like is displayed on all cells at once, but the like is saved after restarting.
I want the like to be only on the cell I clicked on and it will be saved after restarting the application.
Short answer you need something like this.
Button("add", action: {
//Create a new object
let new: LikedDBE = store.create()
//Trigger a child view that observes the object
selection = new.objectID
})
It is a button that creates the object and triggers a child view so you can observe and edit it.
Long answer will be below just copy and paste all the code into your ContentView file there are comment throughout.
import SwiftUI
import CoreData
struct ContentView: View {
#StateObject var store: CoreDataPersistence = .init()
var body: some View{
LikesListView()
//This context is aware of you are in canvas/preview
.environment(\.managedObjectContext, store.context)
}
}
struct LikesListView: View {
#EnvironmentObject var store: CoreDataPersistence
#FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \LikedDBE.name, ascending: true)]) var manyLikedDB: FetchedResults<LikedDBE>
//Control what NavigationLink is opened
#State var selection: NSManagedObjectID? = nil
var body: some View {
NavigationView{
List{
ForEach(manyLikedDB){ object in
NavigationLink(object.name ?? "no name", tag: object.objectID, selection: $selection, destination: {LikeEditView(obj: object)})
}
}.toolbar(content: {
Button("add", action: {
//Create a new object
let new: LikedDBE = store.create()
//Trigger a child view that observes the object
selection = new.objectID
})
})
}.environmentObject(store)
}
}
struct LikeEditView: View{
#EnvironmentObject var store: CoreDataPersistence
#Environment(\.dismiss) var dismiss
//Observe the CoreData object so you can see changes and make them
#ObservedObject var obj: LikedDBE
var body: some View{
TextField("name", text: $obj.name.bound).textFieldStyle(.roundedBorder)
Toggle(isOn: $obj.isLiked, label: {
Text("is liked")
})
.toolbar(content: {
ToolbarItem(placement: .navigationBarLeading){
Button("cancel", role: .destructive, action: {
store.resetStore()
dismiss()
})
}
ToolbarItem(placement: .navigationBarTrailing){
Button("save", action: {
store.update(obj)
dismiss()
})
}
})
.navigationBarBackButtonHidden(true)
}
}
struct LikesListView_Previews: PreviewProvider {
static var previews: some View {
LikesListView()
}
}
///Generic CoreData Helper not needed jsuto make stuff easy.
class CoreDataPersistence: ObservableObject{
//Use preview context in canvas/preview
//The context is for both Entities,
let context = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" ? PersistenceController.preview.container.viewContext : PersistenceController.shared.container.viewContext
///Non observing array of objects
func getAllObjects<T: NSManagedObject>() -> [T]{
let listRequest = T.fetchRequest()
do {
return try context.fetch(listRequest).typeArray()
} catch let error {
print ("Error fetching. \(error)")
return []
}
}
///Creates an NSManagedObject of any type
func create<T: NSManagedObject>() -> T{
T(context: context)
//Can set any defaults in awakeFromInsert() in an extension for the Entity
//or override this method using the specific type
}
///Updates an NSManagedObject of any type
func update<T: NSManagedObject>(_ obj: T){
//Make any changes like a last modified variable
save()
}
///Creates a sample
func addSample<T: NSManagedObject>() -> T{
let new: T = create()
//Can add sample data here by type checking or overriding this method
return new
}
///Deletes an NSManagedObject of any type
func delete(_ obj: NSManagedObject){
context.delete(obj)
save()
}
func resetStore(){
context.rollback()
save()
}
private func save(){
do{
try context.save()
}catch{
print(error)
}
}
}
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue
}
}
}
Does anyone know why this code cause a fatal error: Index out of range, when I try to delete an item from the list? At the moment I am able to create more textfields and populate them but unable to delete anything without the app crashing.
import SwiftUI
struct options: View {
#State var multiOptions = [""]
var body: some View {
VStack {
List {
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
}
.onDelete(perform: removeRow)
}
Button {
multiOptions.append("")
} label: {
Image(systemName: "plus.circle")
}
}
}
func removeRow(at offsets: IndexSet) {
multiOptions.remove(atOffsets: offsets)
}
}
Here is the answer with custom Binding:
struct ContentView: View {
#State var multiOptions = [""]
var body: some View {
VStack {
List {
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: Binding(get: { return multiOptions[index] },
set: { newValue in multiOptions[index] = newValue }))
}
.onDelete(perform: removeRow)
}
Button {
multiOptions.append("")
} label: {
Image(systemName: "plus.circle")
}
}
}
func removeRow(at offsets: IndexSet) {
multiOptions.remove(atOffsets: offsets)
}
}
This seems hard to believe, but apparently the naming of an attribute in the entity as "id" is the cause of this behavior. I changed the name of the UUID attribute to "myID" and the deletions now work. The list view still does not work at all in the preview, but it does now work in the simulator and with a device.
Overview
im doing a simple app with core data I have two entity users and territory the app shows a list of the users in sections by territory the problem is In the delete action the list delete the user from the first section if I try to delete the second user from the second section it delete the second user from the first section.
I think index set is getting wrong sending the index of the section but when I try to change the onDelete to my nested forEach don't work
Here is the code
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: User.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \User.name, ascending: true)]) var users: FetchedResults<User>
#FetchRequest(entity: Territory.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Territory.name, ascending: true)]) var territories: FetchedResults<Territory>
#State private var showAddUser = false
var body: some View {
GeometryReader{ geometry in
NavigationView {
ZStack {
List {
ForEach(self.territories, id: \.self) { territorie in
Section(header: Text(territorie.wrappedName)) {
ForEach(territorie.usersArray, id: \.self) { user in
NavigationLink(destination: UserView(user: user)) {
VStack{
HStack{
Text("user")
Spacer()
Text(user.dayLastVisit)
.padding(.horizontal)
}
HStack {
Text(user.wrappedEmoji)
.font(.largeTitle)
VStack(alignment: .leading) {
Text("\(user.wrappedName + " " + user.wrappedLastName)")
.font(.headline)
Text(user.wrappedType)
}
Spacer()
}
}
}
}.onDelete(perform: self.deleteItem)
}
}
}
.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
VStack {
Button(action:{ self.showAddRUser.toggle()}){
ButtonPlus(icon:"plus")}
.offset(x: (geometry.size.width * 0.40), y: (geometry.size.height * 0.38))
.sheet(isPresented: self.$showAddUser){
NewUserView().environment(\.managedObjectContext, self.moc)
}
}
}
.navigationBarTitle("Users")
.navigationBarItems( trailing: HStack {
EditButton()
Button(action:{self.showAddUser.toggle()}){
ButtonNew(text:"Nueva")}
}
.sheet(isPresented: self.$showAddUser){
NewUserView().environment(\.managedObjectContext, self.moc)
}
)
}
}
}
func deleteItem(at offsets: IndexSet) {
for offset in offsets {
let user = users[offset]
//borarlo del context
moc.delete(user)
}
try? moc.save()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
im learning swift and swiftui so im would appreciate any help
You’ll need to pass in a section index as well as the row index, so that you know which nested item to delete. Something like this.
.onDelete { self.deleteItem(at: $0, in: sectionIndex) }
And change your function to accept that section index:
func deleteItem(at offsets: IndexSet, in: Int)
In your case you can probably pass in something like territorie.id as the section index, and use that to delete the correct item. Or pass in the territorie object - whatever you need to get to the correct user. Only the index won’t get you there. Hope it all makes sense!
For me, the solution was the following:
ForEach(self.territories, id: \.self) { territorie in
Section(header: Text(territorie.wrappedName)) {
ForEach(territorie.usersArray, id: \.self) { user in
// your code here
}
.onDelete { indexSet in
for index in indexSet {
moc.delete(territorie[user])
}
// update the view context
moc.save()
}
}
}
The index in indexSet returns the item that should be deleted in that specific section. So if I delete the first item of a section, it returns 0.
The territorie returns a list of all the items that are contained in that section. So using territorie[index] will return the specific user object you want to delete.
Now that we have the object we want to delete, we can pass it to moc.delete(territorie[index]). Finally, we save it with moc.save().
Sidenote: although Misael used the variable 'territorie', I prefer to use the variable name section.
So thanks to the help of Kevin Renskers who found a solution. I just add a .onDelete { self.deleteItem(at: $0, in: territorie)} to my function then I use the same arrayUsers from the territory.
func deleteItem(at offsets: IndexSet, in ter: Territory) {
for offset in offsets {
let user = ter.usersArray[offset]
moc.delete(user)
}
try? moc.save()
}