onAppear is causing problem with the preview but no error is shown - core-data

self learning beginner here.
When I remove .onAppear{add()}, the preview works fine. I tried to attach it to other the body view, the Vstack but it causes another error. I read/watched several tutorials but nothing like this is mentioned....
Any help is appreciated
struct ListView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var targets: FetchedResults<TargetEntity>
#FetchRequest(sortDescriptors: []) var positives: FetchedResults<PositiveEntity>
var body: some View {
VStack {
Text("+")
.onAppear{add()}
.onTapGesture (count: 2){
do {
increment(targets.first!) //I also sense that doing "!" is not good. But it's the only way I can keep it from causing error "Cannot convert value of type 'FetchedResults' to expected argument type 'X'"
try moc.save()
} catch {
print("error")
}
}
}
}
func increment(_ item: TargetEntity) {
item.countnum += 1
save()
}
func add() {
let countnum = TargetEntity(context: moc)
countnum.countnum = 0
save()
}
func save() {
do { try moc.save() } catch { print(error) }
}
}
EDIT 20220509:
As advised by #Yrb (great thanks), the error is likely caused by the lack of a proper set up of preview var in the persistence file. I post the relevant code here for visiblity.
Data Controller file
import CoreData
import Foundation
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "CounterLateApr")
init () {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
preview code in a view
struct ListView_Previews: PreviewProvider {
static var previews: some View {
NavigationView{
ListView()
}
}
}
[AppName].app file
import SwiftUI
#main
struct CounterLateAprApp: App {
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}

Related

SwiftUI CoreData master detail questions

I'm trying to create a master detail relationship with CoreData. I have a settings tab that is used to select the master (it's global and not done very often by the user). There is another tab that shows the detail entries for the current master.
The master has one field, name, a string and the details array. The detail has one field, name, a string. I'm using UUID().uuidString to populate the names for the example.
The problem I'm having is that when I select the detail tab, it shows the details for the current master. If I add details (click the + button) they do not appear until I change the master (settings -> select master). If I edit the details and delete some, the list entries go away but when I finish editing, they immediately come back. I can switch masters and then go back to the edited master and the data looks correct (I have to change the activeMaster published property).
I'm thinking that the published property isn't forcing the update to the details view because swift doesn't see the master variable change. I may also not be adding or deleting the details correctly.
How is adding details to a master typically done (here master is one to many details)
How is deleting details from a master typically done?
Is the data no showing up due to the published property not "publishing" Any ideas on how to better do this?
Thanks.
Code is below.
Here's the global application data:
import Foundation
import CoreData
import SwiftUI
class ApplicationData: ObservableObject
{
let container: NSPersistentContainer
#Published var activeMaster: Master?
init(preview: Bool = false)
{
container = NSPersistentContainer(name: "MasterDetail")
if (preview)
{
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler:
{ storeDescription, error in
if let error = error as NSError?
{
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
Just persistence and a single optional active master. The application data is created in the application code and set as an environment object:
import SwiftUI
#main
struct MasterDetailApp: App
{
#StateObject var appData = ApplicationData()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(appData)
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
}
The tab view:
import Foundation
import SwiftUI
struct MainView: View
{
#AppStorage("selectedTab") var selectedTab: Int = 0
#EnvironmentObject var appData: ApplicationData
var body: some View
{
TabView(selection: $selectedTab)
{
DetailView()
.tabItem({Label("Detail", systemImage: "house")})
.tag(0)
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1)
}
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
The detail tab allows the user to add details and to edit the list:
import Foundation
import SwiftUI
import CoreData
struct DetailView: View
{
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.dismiss) var dismiss
#EnvironmentObject var appData: ApplicationData
var body: some View
{
NavigationView
{
List
{
ForEach(appData.activeMaster?.wrappedDetail ?? [])
{
detail in Text(detail.name ?? "None")
}
.onDelete(perform: { indexes in Task(priority: .high) { await deleteDetails(indexes: indexes) } } )
}
.toolbar
{
ToolbarItem(placement: .navigationBarTrailing)
{
EditButton()
}
ToolbarItem(placement: .navigationBarTrailing)
{
Button
{
let detail = Detail(context: viewContext)
detail.name = UUID().uuidString
detail.master = appData.activeMaster
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
.disabled(appData.activeMaster == nil)
}
}
}
}
/*
* Delete indexes - assumes that appData.activeWeapon is set.
*/
private func deleteDetails(indexes: IndexSet) async
{
await viewContext.perform
{
for index in indexes
{
print(index)
viewContext.delete(appData.activeMaster!.wrappedDetail[index])
}
do
{
try viewContext.save()
}
catch
{
print("Error deleting dope entry")
}
}
}
}
The settings view just has a navigation link to a view to select the master and an add button to add masters:
import Foundation
import SwiftUI
struct SettingsView: View
{
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var appData: ApplicationData
var body: some View
{
NavigationView
{
Form
{
Section(header: Text("Masters"))
{
NavigationLink(destination: SelectMastersView(selectedMaster: $appData.activeMaster), label:
{
Text(appData.activeMaster?.name ?? "Select Master")
})
Button
{
let master = Master(context: viewContext)
master.name = UUID().uuidString
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
}
}
}
}
}
The view for selecting the master just has a fetch request to get all masters and assign the selected one to the global app data published property:
import Foundation
import SwiftUI
struct SelectMastersView: View
{
#Environment(\.dismiss) var dismiss
#FetchRequest(entity: Master.entity(), sortDescriptors: [], animation: .default)
var masters: FetchedResults<Master>
#Binding var selectedMaster: Master?
var body: some View
{
List
{
ForEach(masters)
{ master in
Text(master.name ?? "None")
.onTapGesture
{
selectedMaster = master
dismiss()
}
}
}
.navigationBarTitle("Masters")
}
}
Edited to add extension to Master I forgot to post.
import Foundation
extension Master
{
var wrappedDetail: [Detail]
{
detail?.allObjects as! [Detail]
}
}
I finally figured it out this morning. I think putting the example code together last night helped quite a bit.
I got it work by creating fetch request in the detail view and passing the master into the view in init().
Here's the updated code for the tab view:
import Foundation
import SwiftUI
struct MainView: View
{
#AppStorage("selectedTab") var selectedTab: Int = 0
#EnvironmentObject var appData: ApplicationData
var body: some View
{
TabView(selection: $selectedTab)
{
DetailView(master: appData.activeMaster)
.tabItem({Label("Detail", systemImage: "house")})
.tag(0)
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1)
}
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
and the updated detail view:
import Foundation
import SwiftUI
import CoreData
struct DetailView: View
{
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.dismiss) var dismiss
#EnvironmentObject var appData: ApplicationData
#FetchRequest(entity: Detail.entity(), sortDescriptors: [])
var details: FetchedResults<Detail>
let master: Master?
init(master: Master?)
{
self.master = master
if master != nil
{
let predicate = NSPredicate(format: "%K == %#", #keyPath(Detail.master), master ?? NSNull())
_details = FetchRequest(sortDescriptors: [], predicate: predicate)
}
}
#ViewBuilder
var body: some View
{
NavigationView
{
List
{
if master != nil
{
ForEach(details)
{
detail in Text(detail.name ?? "None")
}
.onDelete(perform: { indexes in Task(priority: .high) { await deleteDetails(indexes: indexes) } } )
}
}
.toolbar
{
ToolbarItem(placement: .navigationBarTrailing)
{
EditButton().disabled(master == nil || details.isEmpty)
}
ToolbarItem(placement: .navigationBarTrailing)
{
Button
{
let detail = Detail(context: viewContext)
detail.name = UUID().uuidString
detail.master = appData.activeMaster
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
.disabled(appData.activeMaster == nil)
}
}
}
}
/*
* Delete indexes - assumes that appData.activeWeapon is set.
*/
private func deleteDetails(indexes: IndexSet) async
{
await viewContext.perform
{
for index in indexes
{
print(index)
viewContext.delete(appData.activeMaster!.wrappedDetail[index])
}
do
{
try viewContext.save()
}
catch
{
print("Error deleting dope entry")
}
}
}
}
This is not as clean as I'd like. I had to move to a view build for the list. I'd like to be able to create an empty fetch request so I don't have to use a view builder.

Fetching data in Preview: "A fetch request must have an entity UncaughtExceptionError"

I have a ListView file which can build the preview code fine without the Core Data piece in it. However, when I build that ListView in the ContentView, I got an error that is not shown in the text editor like usual but in the diagnostics window. The error is "A fetch request must have an entity UncaughtExceptionError: [AppName] crashed due to an uncaught exception".
When I run the app in the simulator, the ContentView seems to build fine. (the functions attached to Text("+") don't work either, something about "map table argument is NULL", but one error at a time.)
EDIT: The entities in Core Data are set up as “Class Definition”.
Many many thanks in advance.
struct ListView: View {
#Environment(\.managedObjectContext) var viewContext
#FetchRequest(sortDescriptors: []) var targets: FetchedResults<TargetEntity>
#FetchRequest(sortDescriptors: []) var positives: FetchedResults<PositiveEntity>
var body: some View {
VStack {
HStack {
VStack {
Text("+")
.onAppear{add()}
.onTapGesture (count: 2){
do {
print("error")
increment(targets.first!)
try viewContext.save()
} catch {
print("error")
}
}
.onLongPressGesture {
addPositive()
}
List{
ForEach(positives) { item in
Text(item.title ?? "Unknown Title")
}
.onDelete(perform: deleteItems)
}
}
}
}
}
func add() {
let countnum = TargetEntity(context: viewContext)
countnum.countnum = 0
save()
}
func addPositive(){
let newPositive = PositiveEntity(context: viewContext)
newPositive.title = "Action"
save()
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { positives[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
func save() {
do { try viewContext.save() } catch { print(error) }
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
NavigationView{
ListView()//.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
}
Just faced same issue after updating core data model with new entity, in a project created with core data.
Error happens only to "canvas" preview, with simulator or real devices everything is ok.
Issue resolved for me by adding mock of created model to PersistenceController under the preview var

Core Data: how to have a default value

self learning beginner here.
Although in the xcdatamodel file I have the default value for the attribute of that entity (see screenshot), when I build the preview, the Text("+") isn't there. I guess the countnum attribute in the TargetEntity is still empty. My thinking is to have the add() run if the attribute is empty. But that doesn't work either. Is there a way to automatically initialize the attribute when the app runs, instead of needing to build a button for the user to press?
Thanks a million
struct ListView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var targets: FetchedResults<TargetEntity>
var body: some View {
VStack {
if let firstItem = targets.first {
Text("+")
.onTapGesture (count: 2){
do {
increment(firstItem)
try moc.save()
} catch {
print(error)
add()
}
}
}
}
}
func increment(_ item: TargetEntity) {
item.countnum += 1
save()
}
func add() {
let countnum = TargetEntity(context: moc)
countnum.countnum = 0
save()
}
func save() {
do { try moc.save() } catch { print(error) }
}
}

SwiftUI: How to show/edit an int from CoreData without being in a List?

self-learning beginner here. I am trying to show an Int from Core Data in a VStack in ContentView, not in a List. But literally all the tutorials I can find about Core Data (tracking Books, Movies, Orders, Students) are using a List to show an array containing an Int. Nothing on showing an Int by itself.
Xcode can build countnum.countnum +=1 with no problem. Seems to me it is reading it fine. But once I try to show it, it just doesn’t work. I’m wrecking my brain here.
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>
var body: some View {
// let countnum = CountNum(context: moc)
VStack{
Text("+")
.padding()
.onTapGesture (count: 2){
let countnum = CountNum(context: moc)
countnum.countnum += 1
}
Text("\(countnum)") //No exact matches in call to instance method 'appendInterpolation'
}
}
}
Thanks
....all the tutorials ... show an array containing an Int. Yes, that's because CoreData
can contain many "objects". You get an array of your CountNum objects when
you do your .....var countnum: FetchedResults<CountNum>. So you need to decide which CountNum you want to
use. For example, if you want to use the first one, then:
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>
var body: some View {
VStack {
if let firstItem = countnum.first {
Text("+")
.padding()
.onTapGesture(count: 2) {
firstItem.countnum += 1
do {
try moc.save()
} catch {
print(error)
}
}
Text("\(firstItem.countnum)").foregroundColor(.green)
}
}
}
}
EDIT-1: adding new CountNum to CoreData example code in the add button.
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>
var body: some View {
Button(action: {add()}) { Text("add new CountNum").foregroundColor(.green) }
.padding(.top, 50)
List {
ForEach(countnum) { item in
HStack {
Text("++")
.onTapGesture(count: 2) { increment(item) }
Text("\(item.countnum)").foregroundColor(.blue)
Text("delete").foregroundColor(.red)
.onTapGesture { delete(item: item) }
}
}
}
}
func increment(_ item: CountNum) {
item.countnum += 1
save()
}
func add() {
let countnum = CountNum(context: moc)
countnum.countnum = 0
save()
}
func delete(item: CountNum) {
moc.delete(item)
save()
}
func save() {
do { try moc.save() } catch { print(error) }
}
}

after delete of item in SwiftUI List (backed by Core Data) getting a "Thread 1: EXC_BAD_INSTRUCTION" error? (code attached)

After completing a delete of a row in a SwiftUI List I am getting a "Thread 1: EXC_BAD_INSTRUCTION" error. It seems the Core Data delete works as after I restart that data has been removed. So maybe something to do with SwiftUI trying to update it's view after the Core Data delete is performed????
Code:
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: GCItem.entity(), sortDescriptors: []) var gcItems: FetchedResults<GCItem>
private func addItem(title:String) {
let newItem = GCItem(context: context)
newItem.id = UUID()
newItem.title = title
do {
try context.save()
} catch let e as NSError {
fatalError("Unresolved error \(e), \(e.userInfo)")
}
}
private func deleteItem(at offsets:IndexSet) {
self.context.perform {
// Delete Item
for index in offsets {
let item = self.gcItems[index]
self.context.delete(item)
}
// Persist
do {
try self.context.save()
} catch let e as NSError {
// TODO: How to undelete list???
print("ERROR : Can not save GCItem items: \(e.description)")
}
}
}
var body: some View {
NavigationView {
VStack {
List() {
ForEach(gcItems) { gcItem in
HStack {
Text("test")
}
}
.onDelete(perform: self.deleteItem)
}
Button(action: { self.addItem(title: "Testing 123") }) {
Text("ADD ITEM")
}
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
#endif
Screen Shot
The fix was NOT to use attributes in Core Data named "id". So I changed this to "myId" and then things worked fine.

Resources