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()
}
Related
First time post in here and new to coding... so I hope I am following proper protocol. I am putting together a view in Xcode 12.2 (SwiftUI 2) that outputs a list of data from Core Data and have a context menu to provide the user the option to edit, delete, and delete multiple. The context menu is working properly for edit and delete, however, I am facing a road block in how to implement the functionality to delete multiple list items. I am imagining the user would hard press one of the list items, the context menus pops open and if they press the "Delete Multiple" option, the view activates something similar to an edit mode that populates little circle on the left of each item which the user can select and delete more than one item at a time. I can see other article on how to do this, however, I cannot find guidance on how to implement this through Core Data. I have pasted my code below.
Please let me know if I am missing any other information that would make my question more clear.
I really appreciate the forums expertise and guidance.
Struct List : View {
#StateObject var appData = AppViewModel()
#Environment(\.managedObjectContext) var context
//Fetch Data...
#FetchRequest(entity: EntryData.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], animation: .spring()) var results : FetchedResults<EntryData>
var body : some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom), content: {
VStack{
ScrollView(.vertical, showsIndicators: false, content: {
LazyVStack(alignment: .leading, spacing: 20){
ForEach(results){task in
VStack(alignment: .leading, spacing: 5, content: {
Text(task.category ?? "")
.font(.title)
.fontWeight(.bold)
Text(task.date ?? Date(), style:. date)
.fontWeight(.bold)
Text("\(task.number.formattedCurrencyText)")
})
.padding(.horizontal, 14)
.padding(.top, 10)
.foregroundColor(Color("ColorTextList"))
.contextMenu{
Button(action: {appData.editItem(item: task)}, label: {
Text("Edit")
})
Button(action: {
context.delete(task)
try! context.save()
}, label: {
Text("Delete")
})
Button(action: {}, label: {
Text("Delete Mutiple")
})
}
}
}
})
}
VStack(){
VisualEffectView(effect: UIBlurEffect(style: .regular))
.frame(width: UIScreen.main.bounds.width, height: 50, alignment: .top)
.edgesIgnoringSafeArea(.all)
.background(Color.clear)
Spacer()
}
})
.background(Color.clear)
.sheet(isPresented: $appData.isNewData, content: {
AddDataView(appData: appData)
.environment(\.managedObjectContext, self.context)
})
}
}
Adding the viewModel of the app. How do I tap into into this and delete each of the attributes in a multi-list selection?
import SwiftUI
import CoreData
class AppViewModel : ObservableObject, Identifiable{
#Published var cateogry = ""
#Published var date = Date()
#Published var number : Double? = nil
#Published var notes = ""
#Published var id = UUID()
}
And adding the actual Core Data model screenshot.
You can implement a Selection Set for your List. This will contain all elements that are selected. Then you can dynamically show the contextMenu for delete or deleteAll based on the count of the set. Here is a full example with the implementation of deleteAll
struct SelectionDemo : View {
#State var demoData = ["Dave", "Tom", "Phillip", "Steve"]
#State var selected = Set<String>()
var body: some View {
HStack {
List(demoData, id: \.self, selection: $selected){ name in
Text(name)
.contextMenu {
Button(action: {
//Delete only one item
}){
Text("Delete")
}
if (selected.count > 1) {
Button(action: {
//Delete all
deleteAll()
})
{
Text("Delete all")
}
}
}
}.frame(width: 500, height: 460)
}
}
func deleteAll() {
for element in selected {
self.demoData.removeAll(where: {
$0 == element
})
}
}
}
I followed this document (and this one) to add a delete feature to my list in an app using SwiftUI. Both pages say that once you add the .onDelete(perform: ...) piece of code you will be able to swipe and get a Delete button. Nevertheless this is not what I see. The code compiles but I see nothing on swipe.
My list is backed up by code like this:
#FetchRequest(
entity: ...,
sortDescriptors: []
) var myList: FetchedResults<MyEntity>
and not by #State. Could this be an issue?
Below follows more of the relevant code, in case this may be useful:
private func deleteSpot(at index: IndexSet) {
print(#function)
}
.........
var body: some View {
VStack {
ForEach(self.myList, id: \.self.name) { item in
HStack {
Spacer()
Button(action: {
self.showingDestinList.toggle()
.....
UserDefaults.standard.set(item.name!, forKey: "LocSpot")
}) {
item.name.map(Text.init)
.font(.largeTitle)
.foregroundColor(.secondary)
}
Spacer()
}
}.onDelete(perform: deleteSpot)
}
The delete on swipe for dynamic container works only in List, so make
var body: some View {
List { // << here !!
ForEach(self.myList, id: \.self.name) { item in
HStack {
// ... other code
}
}.onDelete(perform: deleteSpot)
}
}
By searching and trying various options, I ended up by finding the issue.
I find the solution somewhat ridiculous, but to avoid other people to lose time, here it is:
The last part of the code in the post needs to be modified like the following in order to work.
var body: some View {
VStack {
List {
ForEach(self.myList, id: \.self.name) { item in
HStack {
Spacer()
Button(action: {
self.showingDestinList.toggle()
.....
UserDefaults.standard.set(item.name!, forKey: "LocSpot")
}) {
item.name.map(Text.init)
.font(.largeTitle)
.foregroundColor(.secondary)
}
Spacer()
}
}.onDelete(perform: deleteSpot)
}
}
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
}
}
I am trying to make an ordered list in SwiftUI using CoreData records.
How to print running numbers in such list?
In the following example I have one entity named SomeEntity with a String attribute named title.
import SwiftUI
struct ContentView: View {
var fetchRequest: FetchRequest<SomeEntity>
var items: FetchedResults<SomeEntity> { fetchRequest.wrappedValue }
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) {item in
NavigationLink(destination: ItemDetailsView(item: item)) {
HStack {
Text("99")
// How to print running number instead of "99" in this ordered list of CoreData records?
// I was thinking about something like this:
// Text(items.id) - but this doesn't work. Is there something similar?
.multilineTextAlignment(.center)
.frame(width: 60)
Text(item.title!)
}
}
}
}
}
}
}
Probably you need something like the following
struct ContentView: View {
var fetchRequest: FetchRequest<SomeEntity>
var items: FetchedResults<SomeEntity> { fetchRequest.wrappedValue }
var body: some View {
NavigationView {
List {
ForEach(Array(items.enumerated()), id: \.element) {(i, item) in
NavigationLink(destination: ItemDetailsView(item: item)) {
HStack {
Text("\(i + 1)")
.multilineTextAlignment(.center)
.frame(width: 60)
Text(item.title!)
}
}
}
}
}
}
}
Based on your comments, this should work: You need to use a different init of ForEach, which takes a Range<Int> as first argument:
ForEach(-items.count..<0, id: \.self) { i in
NavigationLink(destination: ItemDetailsView(item: items[-i])) {
HStack {
Text("\(items[-i].itemName)")
.multiLineTextAlignment(.center)
.frame(width: 60)
Text("\(items[-i].title!)")
}
}
}
Going from -items.count to 0 also ensures the reversed order.
I've tested it and with #FetchRequest this solution seems to be the best.
List {
ForEach(self.contacts.indices, id: \.self) { i in
Button(action: {
self.selectedId = self.contacts[i].id!
}) {
ContactRow(contact: self.contacts[i])
.equatable()
.background(Color.white.opacity(0.01))
}
.buttonStyle(ListButtonStyle())
.frame(height: 64)
.listRowBackground( (i%2 == 0) ? Color("DarkRowBackground") : .white)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
I've tested also solution with Array(self.contacts.enumerated()) but it doesn't work as well. If you there is small number of records it can be ok but for large number of records it is suboptimal.
If you use
request.fetchBatchSize = 10
to load records (entities) in batches while scrolling the list enumerated() doesn't work executes all needed SELECT ... LIMIT 10 requests at once
.indices makes it possible to fetch additional items while scrolling.
I have a SwiftUI ScrollView with an HStack and a ForEach inside of it. The ForEach is built off of a Published variable from an ObservableObject so that as items are added/removed/set it will automatically update in the view. However, I'm running into multiple problems:
If the array starts out empty and items are then added it will not show them.
If the array has some items in it I can add one item and it will show that, but adding more will not.
If I just have an HStack with a ForEach neither of the above problems occur. As soon as it's in a ScrollView I run into the problems.
Below is code that can be pasted into the Xcode SwiftUI Playground to demonstrate the problem. At the bottom you can uncomment/comment different lines to see the two different problems.
If you uncomment problem 1 and then click either of the buttons you'll see just the HStack updating, but not the HStack in the ScrollView even though you see init print statements for those items.
If you uncomment problem 2 and then click either of the buttons you should see that after a second click the the ScrollView updates, but if you keep on clicking it will not update - even though just the HStack will keep updating and init print statements are output for the ScrollView items.
import SwiftUI
import PlaygroundSupport
import Combine
final class Testing: ObservableObject {
#Published var items: [String] = []
init() {}
init(items: [String]) {
self.items = items
}
}
struct SVItem: View {
var value: String
init(value: String) {
print("init SVItem: \(value)")
self.value = value
}
var body: some View {
Text(value)
}
}
struct HSItem: View {
var value: String
init(value: String) {
print("init HSItem: \(value)")
self.value = value
}
var body: some View {
Text(value)
}
}
public struct PlaygroundRootView: View {
#EnvironmentObject var testing: Testing
public init() {}
public var body: some View {
VStack{
Text("ScrollView")
ScrollView(.horizontal) {
HStack() {
ForEach(self.testing.items, id: \.self) { value in
SVItem(value: value)
}
}
.background(Color.red)
}
.frame(height: 50)
.background(Color.blue)
Spacer()
Text("HStack")
HStack {
ForEach(self.testing.items, id: \.self) { value in
HSItem(value: value)
}
}
.frame(height: 30)
.background(Color.red)
Spacer()
Button(action: {
print("APPEND button")
self.testing.items.append("A")
}, label: { Text("APPEND ITEM") })
Spacer()
Button(action: {
print("SET button")
self.testing.items = ["A", "B", "C"]
}, label: { Text("SET ITEMS") })
Spacer()
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = UIHostingController(
// problem 1
rootView: PlaygroundRootView().environmentObject(Testing())
// problem 2
// rootView: PlaygroundRootView().environmentObject(Testing(items: ["1", "2", "3"]))
)
Is this a bug? Am I missing something? I'm new to iOS development..I did try wrapping the actual items setting/appending in the DispatchQueue.main.async, but that didn't do anything.
Also, maybe unrelated, but if you click the buttons enough the app seems to crash.
Just ran into the same issue. Solved with empty array check & invisible HStack
ScrollView(showsIndicators: false) {
ForEach(self.items, id: \.self) { _ in
RowItem()
}
if (self.items.count == 0) {
HStack{
Spacer()
}
}
}
It is known behaviour of ScrollView with observed empty containers - it needs something (initial content) to calculate initial size, so the following solves your code behaviour
#Published var items: [String] = [""]
In general, in such scenarios I prefer to store in array some "easy-detectable initial value", which is removed when first "real model value" appeared and added again, when last disappears. Hope this would be helpful.
For better readability and also because the answer didn't work for me. I'd suggest #TheLegend27 answer to be slightly modified like this:
if self.items.count != 0 {
ScrollView(showsIndicators: false) {
ForEach(self.items, id: \.self) { _ in
RowItem()
}
}
}