SwiftUI dismiss after list reorder - swift

I'm trying to reorder my CoreData items, but when I try reorder items and drop cell my list view is being dismissed. All changes are saved and displayed correctly in other views and in the list that is modified.
Here's my code
import SwiftUI
struct DaysOrderView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Days.entity(), sortDescriptors:[NSSortDescriptor(key: "id", ascending: true)]) var days : FetchedResults<Days>
var body: some View {
List {
ForEach(days, id: \.self ) { day in
HStack {
Image(systemName: day.isDisplayed.boolValue ? "checkmark.circle.fill" : "checkmark.circle").foregroundColor(.yellow)
Text(day.name)
}
}
.onMove(perform: onMove)
}
.toolbar {
EditButton()
}
}
private func onMove(source: IndexSet, destination: Int) {
var revisedItems: [ Days ] = days.map{ $0 }
revisedItems.move(fromOffsets: source, toOffset: destination )
for reverseIndex in stride( from: revisedItems.count - 1,
through: 0,
by: -1 )
{
revisedItems[ reverseIndex ].id = Int16( reverseIndex )
}
}
}

An id is not arbitrary, and changing id's to change the sort order can have consequences. If you want a changeable order, I would recommend keeping track of it with an attribute dedicated to the sort order and change that.
I put together a quick app using both a sortOrder attribute and an id attribute. When I used 'idas anInt16` and sorted and changed on that, I got the following error:
ForEach<FetchedResults, Int16, NavigationLink<Text, Text>>: the
ID 2 occurs multiple times within the collection, this will give
undefined results!
Swapping it out for a sortOrder attribute gave no errors. Since you didn't, understandably with Core Data, post the entire code, I can't be 100% sure, but I am betting your problem comes down to this issue. Also, you should make your id a UUID() that is what it exists for, and do not change the id. It is like the parents with multiple kids swapping the kids' names on a daily basis. It might work, but most likely it is chaos.
FWIW the code used to test:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.sortOrder, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text("Item \(item.sortOrder) at \(item.timestamp!, formatter: itemFormatter)")
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
.onMove(perform: onMove)
}
.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()
newItem.id = UUID()
newItem.sortOrder = Int16(items.count)
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)")
}
}
}
private func onMove(source: IndexSet, destination: Int) {
var revisedItems: [ Item ] = items.map{ $0 }
revisedItems.move(fromOffsets: source, toOffset: destination )
for reverseIndex in stride( from: revisedItems.count - 1,
through: 0,
by: -1 )
{
revisedItems[ reverseIndex ].sortOrder = Int16( reverseIndex )
}
}
}
It is the base "Use Core Data" code that I added the attributes to.

Related

How to get current IndexSet in a List SwiftUI? [duplicate]

I'm trying to delete a list item with context menu.
The data is fetched from Core Data.
.onDelete works as expected with my deleteExercise func without further do.
But when calling the deleteExercise within the context menu button, it asks for the IndexSet which I honestly have no idea where to get from.
I am also wondering why I don't need to specify the IndexSet when using .onDelete
struct ExercisesView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
animation: .default)
private var exercises: FetchedResults<Exercise>
var body: some View {
NavigationView {
List {
ForEach(exercises) { e in
VStack {
NavigationLink {
ExerciseDetailView(exercise: e)
} label: {
Text(e.name ?? "")
}
}
.contextMenu { Button(role: .destructive, action: { deleteExercise(offsets: /* Index Set */) }) {
Label("Delete Exercise", systemImage: "trash")
} }
}
.onDelete(perform: deleteExercise)
}
}
}
private func deleteExercise(offsets: IndexSet) {
withAnimation {
for index in offsets {
let exercise = exercises[index]
viewContext.delete(exercise)
}
viewContext.save()
}
}
}
Instead of trying to derive an IndexSet from ForEach, which doesn't immediately expose one for you, you could create a separate delete method:
.contextMenu { Button(role: .destructive, action: {
deleteExercise(exercise)
}) {
Label("Delete Exercise", systemImage: "trash")
} }
func deleteExercise(_ exercise: Exercise) { //I'm making an assumption that your model is called Exercise
withAnimation {
viewContext.delete(exercise)
viewContext.save()
}
}
In regards to your last question:
I am also wondering why I don't need to specify the IndexSet when using .onDelete
You don't need to specify it because it's sent as a parameter by onDelete -- that's what your deleteExercise(offsets:) is receiving from the onDelete modifier.

Cannot find 'entity' in scope SwiftUI

I'm trying to display the detail of an item from a list on to a sheet. I'm getting this error Cannot find 'entity' in scope even though it's declared in the detail item struct.
This error doesn't appear if the sheet is inside of the list BUT this causes only the first item detail to be shown even if you select any of the other items below the first.
This is a macOS app.
#StateObject var vm = CoreDataViewModel()
List {
ForEach(vm.savedEntites) { entity in
Text(entity.name ?? "No Name")
.font(.system(size: 25))
HStack {
Button(action: {vm.deleteMemory(entity: entity)}) {
Label("Delete",systemImage: "trash")
}.foregroundColor(Color(.red))
Button(action: {showingDetailScreen.toggle()}) {
Label("Details", systemImage: "pencil")
}.foregroundColor(Color(.red))
}
}// list ends here
.sheet(isPresented: $showingDetailScreen) {
DetailItemView(entity: entity,isVisible: self.$showingDetailScreen)
}
}
Detail Item View Struct
struct DetailItemView: View {
var entity: MemoryEntity
#Environment(\.presentationMode) var presentationMode
#Binding var isVisible: Bool
var body: some View {
Text(entity.name ?? "No Name")
HStack {
Button("Exit") {
self.isVisible = false
}
}
.frame(width: 300, height: 150)
}
}
ViewModel
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntites: [MemoryEntity] = []
#Published var selectedEntity: String = ""
init() {
container = NSPersistentContainer(name: "MemoryContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error loading CoreData. \(error)")
}
}
FetchMemories()
}
func FetchMemories() {
let request = NSFetchRequest<MemoryEntity>(entityName: "MemoryEntity")
do {
savedEntites = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching \(error)")
}
}
}
The scope of entity is inside ForEach brackets,
ForEach(vm.savedEntites) { entity in
} //End scope entity var
If you want to show a present outside ForEach, as a suggestion you could declare in your viewModel a selectedEntity as #Published
List {
ForEach(vm.savedEntites) { entity in
Button(action: {
vm.selectedEntity = entity
showingDetailScreen.toggle()
}) {
}
.sheet(isPresented: $showingDetailScreen) {
DetailItemView(entity: vm.selectedEntity,isVisible: self.$showingDetailScreen)
}

SwiftUI - Involuntary navigation between views

I have two views:
The parent view first decodes a bundled JSON file and then passes an object to the child view. The child view then saves the object (plus a couple additional objects) to Core Data.
That part works fine. The problem is that after saving to Core Data, the child view then navigates back to the parent view and I'm not sure why. Intended behaviour is to navigate back to the root view.
Parent View:
struct SelectCityView: View {
#State private var cities = [Destination]()
#State private var searchText: String = ""
var body: some View {
VStack {
SearchBar(text: $searchText)
List {
// Filter decoded array by search text
ForEach(cities.filter{self.searchFor($0.city)}.sorted(by: { $0.city < $1.city })) { destination in
// Here's where the parent view passes the object to the child view
NavigationLink(destination: SelectTravelDatesView(selectedCity: destination)) {
Text("\(destination.city), \(destination.country)")
}
}
}.id(UUID())
}.navigationTitle("Select city")
.onAppear(perform: {
// Decode JSON data from bundle
cities = Bundle.main.decode([Destination].self, from: "cities.json")
})
}
private func searchFor(_ searchQuery: String) -> Bool {
return (searchQuery.lowercased(with: .current).hasPrefix(searchText.lowercased(with: .current)) || searchText.isEmpty)
}
}
Child View:
struct SelectTravelDatesView: View {
#Environment(\.managedObjectContext) private var viewContext
#State var selectedCity: Destination?
#State private var startDate = Date()
#State private var endDate = Date()
var body: some View {
Form {
Section(header: Text("Trip start date")) {
DatePicker(selection: $startDate, in: Date()..., displayedComponents: .date) {
Text("Trip start date")
}.datePickerStyle(GraphicalDatePickerStyle())
}
Section(header: Text("Trip end date")) {
DatePicker(selection: $endDate, in: startDate..., displayedComponents: .date) {
Text("Trip start date")
}.datePickerStyle(GraphicalDatePickerStyle())
}
}.navigationTitle("Select dates")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
// Save to Core Data
Button(action: {
addItem()
}, label: {
Text("Save")
})
}
}
}
private func addItem() {
withAnimation {
let newItem = Trip(context: viewContext)
if let destination = selectedCity {
newItem.timestamp = Date()
newItem.destination = destination.city
newItem.startDate = startDate
newItem.endDate = endDate
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
}
Any help would be greatly appreciated.
to go back to the root view you can try adding ".isDetailLink(false)" to the NavigationLink.
see
SwiftUI: How to pop to Root view

How to change toggle on just one Core Data item using ForEach in SwiftUI?

How to change just one toggle in a list without subviews? I know how to make it work if I extract Subview from everything inside ForEach, but how to do it on one view?
I cannot use subview, because I have a problem later if I want to delete an item from this subview. It gives me some errors I don't know how to fix, so I am trying to make it on one view where I don't have this error.
The code for the list is quite simple:
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
var fetchRequest: FetchRequest<Item>
var items: FetchedResults<Item> { fetchRequest.wrappedValue }
#State private var doneStatus : Bool = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) {item in
HStack {
Text("\(item.name ?? "default item name")")
Spacer()
Toggle(isOn: self.$doneStatus) {
Text("Done")
}
.labelsHidden()
.onAppear {
self.doneStatus = item.done
}
.onTapGesture {
self.doneStatus.toggle()
item.done.toggle()
try? self.moc.save()
}
}
}
.onDelete(perform: removeItem)
}
.navigationBarTitle("Items")
.navigationBarItems(
leading:
Button(action: {
for number in 1...3 {
let item = Item(context: self.moc)
item.date = Date()
item.name = "Item \(number)"
item.done = false
do {
try self.moc.save()
}catch{
print(error)
}
}
}) {
Text("Add 3 items")
}
)
}
}
init() {
fetchRequest = FetchRequest<Item>(entity: Item.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Item.name, ascending: true)
])
}
func removeItem(at offsets: IndexSet) {
for offset in offsets {
let item = items[offset]
moc.delete(item)
}
try? moc.save()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
//Test data
let testItem = Item.init(context: context)
testItem.date = Date()
testItem.name = "Item name"
testItem.done = false
return ContentView().environment(\.managedObjectContext, context)
}
}
I am using 1 Core Data Entity: Item. With 3 attributes: date (Date), done (Boolean), name (String).
PROBLEM
When I tap on one toggle, all other toggles change as well.
I couldn't find a solution working with Core Data. I guess maybe I should use .id instead of .self? And add another attribute to my entity: id (UUID). But I tried to do it and failed.
I will appreciate any kind of help.
You bound all Toggle to one state... so
remove this
// #State private var doneStatus : Bool = false
bind Toggle dynamically to currently iterating item (note: .onAppear/.onTapGesture not needed anymore)
Toggle(isOn: Binding<Bool>(
get: { item.done },
set: {
item.done = $0
try? self.moc.save()
})) {
Text()
}
.labelsHidden()

onDisappear if Core Data instance exists

TL;DR version:
I am using .onDisappear method in SwiftUI in my app having Core Data. How to make this method conditional if a particular instance of my Entity exists? (Specifically, based on existence of the item details I am looking at, self.item)
--
MORE DETAILS:
Simple example of the problem
Here is a very simple app with a list of items. One Entity is: Item. It has 2 attributes: date (Date), name (String). Please create this Entity if you will use the code below.
There are 2 SwiftUI views:
ContentView.swift
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
var fetchRequest: FetchRequest<Item>
var items: FetchedResults<Item> { fetchRequest.wrappedValue }
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) {item in
NavigationLink(destination: DetailsView(item: item)) {
Text("\(item.name ?? "default item name")")
}
}
}
.navigationBarTitle("Items")
.navigationBarItems(
leading:
Button(action: {
for number in 1...3 {
let item = Item(context: self.moc)
item.date = Date()
item.name = "Item \(number)"
do {
try self.moc.save()
}catch{
print(error)
}
}
}) {
Text("Add 3 items")
}
)
}
}
init() {
fetchRequest = FetchRequest<Item>(entity: Item.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Item.name, ascending: true)
])
}
}
DetailsView.swift
import SwiftUI
struct DetailsView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var item: Item
var body: some View {
VStack {
Text("\(item.name ?? "default item name")")
}
.navigationBarItems(
trailing:
Button(action: {
self.moc.delete(self.item)
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
}catch{
print(error)
}
}) {
Text("Delete")
.foregroundColor(.red)
}
)
//This is causing problems. It tries to save new item name, even if I just deleted this item.
.onDisappear {
self.item.name = "new name"
try? self.moc.save()
}
}
}
What this example app does is:
Creates 3 items in Core Data for testing purposes.
When you tap on one item, you will go to DetailsView.
I want to be able to Delete and item from the DetailsView.
I am aware of this great tutorial from Paul Hudson: https://www.hackingwithswift.com/books/ios-swiftui/deleting-from-a-core-data-fetch-request on how to add Delete on the view with list. And it works great. But this is not what I need for my app.
THE PROBLEM
When I delete an item from the DetailsView, I am getting an error:
020-06-26 19:02:24.534275+0700 list-delete[57017:18389643] [error]
error: Mutating a managed object 0x80784bd2555f567e
x-coredata://99AA316D-D816-49BB-9C09-943F307C3174/Item/p41
(0x600002f03d40) after it has been removed from its context. CoreData:
error: Mutating a managed object 0x80784bd2555f567e
x-coredata://99AA316D-D816-49BB-9C09-943F307C3174/Item/p41
(0x600002f03d40) after it has been removed from its context.
I have eventually figured out that the problem is with .onDisappear method. Which is triggered even after deleting the item.
The main purpose of using .onDisappear here was to make it available to edit an item in DetailsView and .onDisappear would save the changes in CoreData. And this alone works great too.
How I fixed the bug
I have added a #State private var itemExists = true. When I tap Delete button, self.itemExists changes to false. And I have added an IF statement to .onDisappear using this itemExists variable. Updated code of DetailsView looks like this:
import SwiftUI
struct DetailsView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var item: Item
#State private var itemExists = true
var body: some View {
VStack {
Text("\(item.name ?? "default item name")")
}
.navigationBarItems(
trailing:
Button(action: {
self.moc.delete(self.item)
self.itemExists = false
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
}catch{
print(error)
}
}) {
Text("Delete")
.foregroundColor(.red)
}
)
.onDisappear {
if self.itemExists {
self.item.name = "new name"
try? self.moc.save()
}
}
}
}
Now the question is if this additional variable is necessary? Or is there a simpler way of checking if Core Data's Entity instance exists?
I have tried another IF:
if self.item != nil {
But this just gave me a warning:
Comparing non-optional value of type 'Item' to 'nil' always returns
true
isDeleted will be true on any managed object that has been marked for deletion. You can check that instead of tracking with a separate variable. You might want to only save the context on onDisappear instead of after the deletion, though, as the documentation seems to suggest it's intended for use between deleting and saving.