SwiftUI CoreData master detail questions - core-data

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.

Related

onAppear is causing problem with the preview but no error is shown

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

Saving Item in CoreData Initiates Navigation

In my ContentView I have a FetchRequest<Project>. I navigate to ProjectView using a NavigationLink. From ProjectView I navigate to AddItemView using another NavigationLink. In AddItemView when I add an Item to the Project and call container.viewContext.save() the AddItemView automatically dismisses back to the ContentView.
My guess is that saving to CoreData updates the FetchRequest<Project> list which in turn updates the views, but I am not sure.
How can I save a new Item to the Project in CoreData and only navigate back to ProjectView and not ContentView?
To reproduce:
Create new Single View App and check Core Data and Host in CloudKit
In the .xcdatamodel delete the default Entity and replace it with an Entity called Project which has attributes date: Date and title: String and an Entity called Item which has an attribute name: String. Give the Project a relationship called items (to type Item) and choose “to many” on the right. Give the Item a relationship called project that is the inverse of items.
Replace the code in Persistence.swift with this:
// Persistence.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "CoreDataBug")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// 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.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
Copy ContentView
// ContentView.swift
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
let projects: FetchRequest<Project>
init() {
projects = FetchRequest<Project>(entity: Project.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Project.date, ascending: false)
])
}
var body: some View {
NavigationView {
List {
ForEach(projects.wrappedValue) { project in
NavigationLink(destination: ProjectView(project: project)) {
Text(project.title ?? "Title")
}
}
}
.navigationTitle("Projects")
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
Button {
withAnimation {
let project = Project(context: moc)
let now = Date()
project.date = now
project.title = now.description
try? moc.save()
}
} label: {
Label("Add Project", systemImage: "plus")
}
}
}
}
}
}
Create ProjectView.swift and copy this:
// ProjectView.swift
import SwiftUI
struct ProjectView: View {
#ObservedObject var project: Project
var items: [Item] {
project.items?.allObjects as? [Item] ?? []
}
var body: some View {
List {
ForEach(items) { item in
Text(item.name ?? "")
}
}
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
NavigationLink(destination: AddItemView(project: project)) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
Create AddItemView.swift and copy this:
import SwiftUI
// AddItemView.swift
import SwiftUI
struct AddItemView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var moc
let project: Project
#State private var selectedName: String = ""
var body: some View {
TextField("Type name here", text: $selectedName)
.navigationTitle("Add Item")
.navigationBarItems(trailing: Button("Add") {
let ingestion = Item(context: moc)
ingestion.project = project
ingestion.name = selectedName
try? moc.save()
presentationMode.wrappedValue.dismiss()
})
}
}
Run the app. Click the plus on the top right. Click the project that just slid in. In the ProjectView click the plus on the top right again. Type a name in the TextField and click add on the top right. When the AddItemView is dismissed it probably went back to ContentView. If not add another item to the project.

SwiftUI / Core Data - Involuntary navigation between list view and detailed view when updating detailed view

I am using a TabView/NavigationView & NavigationLink to programmatically navigate from a list view to detailed view and when I update the boolean property 'pinned' to true & save the core data entity in the detailed view, I am getting the following unfortunate side effects:
an involuntary navigation back to the list view and then again back to the detailed view or
an involuntary navigation to another copy of the same detailed view and then back to the list view.
I have prepared a small Xcode project with the complete sample code
In the list view I use #FetchRequest to query the list and sort on the following:
#FetchRequest(entity: Task.entity(),sortDescriptors: [NSSortDescriptor(key: "pinned", ascending: false),
NSSortDescriptor(key: "created", ascending: true),
NSSortDescriptor(key: "name", ascending: true)])
In the list view I use the following:
List() {
ForEach(tasks, id: \.self) { task in
NavigationLink(destination: DetailsView(task: task), tag: task.id!.uuidString, selection: self.$selectionId) {
HStack() {
...
}
}
}
.
(1) If I omit the 'NSSortDescriptor(key: "pinned" ...)' I don't see the behavior.
(2) If I omit the 'tag:' and the 'selection:' parameters in the NavigationLink() I don't see the behavior. But I need to be able to trigger the navigation link programmatically when I create a new Task entity.
(3) It seems never to happen when I have a single entity in the list or changing the value of the 'pinned' boolean property in the first entity in the list.
(4) I get the warning:
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window)...
The parent view to the list view (TasksListView) contains a TabView:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TasksListView()
}
.tabItem {
Image(systemName: "tray.full")
.font(.title)
Text("Master")
}
NavigationView {
EmptyView()
}
.tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
Text("Search")
}
}
}
}
struct TasksListView: View {
// NSManagedObjectContext
#Environment(\.managedObjectContext) var viewContext
// Results of fetch request for tasks:
#FetchRequest(entity: Task.entity(),sortDescriptors: [NSSortDescriptor(key: "pinned", ascending: false),
NSSortDescriptor(key: "created", ascending: true),
NSSortDescriptor(key: "name", ascending: true)])
var tasks: FetchedResults<Task>
// when we create a new task and navigate to it programitically
#State var selectionId : String?
#State var newTask : Task?
var body: some View {
List() {
ForEach(tasks, id: \.self) { task in
NavigationLink(destination: DetailsView(task: task), tag: task.id!.uuidString, selection: self.$selectionId) {
HStack() {
VStack(alignment: .leading) {
Text("\(task.name ?? "unknown")")
.font(Font.headline.weight(.light))
.padding(.bottom,5)
Text("Created:\t\(task.created ?? Date(), formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
.padding(.bottom,5)
if task.due != nil {
Text("Due:\t\t\(task.due!, formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
.padding(.bottom,5)
}
}
}
}
}
}
.navigationBarTitle(Text("Tasks"),displayMode: .inline)
.navigationBarItems(trailing: rightButton)
}
var rightButton: some View {
Image(systemName: "plus.circle")
.foregroundColor(Color(UIColor.systemBlue))
.font(.title)
.contentShape(Rectangle())
.onTapGesture {
// create a new task and navigate to it's detailed view to add values
Task.create(in: self.viewContext) { (task, success, error) in
if success {
self.newTask = task
self.selectionId = task!.id!.uuidString
}
}
}
}
}
struct DetailsView: View {
// NSManagedObjectContext
#Environment(\.managedObjectContext) var viewContext
#ObservedObject var task : Task
#State var name : String = ""
#State var dueDate : Date = Date()
#State var hasDueDate : Bool = false
#State var isPinned : Bool = false
var body: some View {
List() {
Section() {
Toggle(isOn: self.$isPinned) {
Text("Pinned")
}
}
Section() {
TextField("Name", text: self.$name)
.font(Font.headline.weight(.light))
Text("\(task.id?.uuidString ?? "unknown")")
.font(Font.headline.weight(.light))
}
Section() {
HStack() {
Text("Created")
Spacer()
Text("\(task.created ?? Date(), formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
}
Toggle(isOn: self.$hasDueDate) {
Text("Set Due Date")
}
if self.hasDueDate {
DatePicker("Due Date", selection: self.$dueDate, in: Date()... , displayedComponents: [.hourAndMinute, .date])
}
}
}
.navigationBarTitle(Text("Task Details"),displayMode: .inline)
.navigationBarItems(trailing: rightButton)
.listStyle(GroupedListStyle())
.onAppear() {
if self.task.pinned {
self.isPinned = true
}
if self.task.name != nil {
self.name = self.task.name!
}
if self.task.due != nil {
self.dueDate = self.task.due!
self.hasDueDate = true
}
}
}
// save button
var rightButton: some View {
Button("Save") {
// save values in task & save:
self.task.pinned = self.isPinned
if self.hasDueDate {
self.task.due = self.dueDate
}
if self.name.count > 0 {
self.task.name = self.name
}
Task.save(in: self.viewContext) { (success, error) in
DispatchQueue.main.async {
if success {
print("Task saved")
}
else {
print("****** Error: Task can't be saved, error = \(error!.localizedDescription)")
}
}
}
}
.contentShape(Rectangle())
}
}
extension Task {
static func save(in managedObjectContext: NSManagedObjectContext, completion: #escaping (Bool, NSError?) -> Void ) {
managedObjectContext.performAndWait() {
do {
try managedObjectContext.save()
completion(true, nil)
} catch {
let nserror = error as NSError
print("****** Error: Unresolved error \(nserror), \(nserror.userInfo)")
completion(false, nserror)
}
}
}
}
Any suggestions?
You seem to be creating your tab view in a different way than me. Those extra navigation views made cause an issue.
Not sure if it will help or not but I do it like that:
struct ContentView: View {
var body: some View {
TabView {
TasksListView()
.tabItem {
Image(systemName: "tray.full")
.font(.title)
Text("Master")
}
EmptyView()
.tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
Text("Search")
}
}
}
}
This is a bug in iOS 13.
It has been fixed since iOS 14.0 beta 3.
You will find a similar question here (the accepted answer provides a workaround):
Issue when rearranging List item in detail view using SwiftUI Navigation View and Sorted FetchRequest

iOS 13.4 CoreData SwiftUI app crashes with "EXC_BREAKPOINT (code=1, subcode=0x1f3751f08)" on device

A very simple CoreData app: All code provided below.
Start up with CoreData template single view app.
2 entities with a string attribute each: Message(title) and Post(name)
A NavigationView containing
NavigationLink to a list of messages
NavigationLink to a list of posts
Each linked ListView (Message/Post) has
a button to add an item to the list
a button to remove all items from the list
Now, when you run this app on a simulator (any iOS 13.x version) all runs as expected from the description above.
But on a DEVICE running iOS 13.4
Tap "Messages"
Creating/deleting messages works fine, SwiftUi view updates immediately.
Tap "back"
Tap "Messages" again. While still creating/deleting messages works fine: The debugger now shows a warning: "Context in environment is not connected to a persistent store coordinator: NSManagedObjectContext: 0x280ed72c0
Tap "Posts"
==> App crashes with EXC_BREAKPOINT (code=1, subcode=0x1f3751f08)
You can start the process with Posts first, too. Then the same crash occurs on the messages list view.
I strongly believe this is an iOS 13.4 bug because similar code ran fine on Xcode 11.3 / iOS 13.3.
Does anyone know a fix or workaround for this?
Here is a link to the full project: Full Xcode Project
The ContentView:
import SwiftUI
import CoreData
struct MessageList: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Message.entity(), sortDescriptors: [])
var messages: FetchedResults<Message>
var body: some View {
List() {
ForEach(messages, id: \.self) { message in
Text(message.title ?? "?")
}
}
.navigationBarItems(trailing:
HStack(spacing: 16) {
Button(action: deleteMessages) {
Image(systemName: "text.badge.minus")
}
Button(action: addMessage) {
Image(systemName: "plus.app")
}
}
)
}
func addMessage() {
let m = Message(context: moc)
m.title = "Message: \(Date())"
try! moc.save()
}
func deleteMessages() {
messages.forEach {
moc.delete($0)
}
}
}
struct PostList: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Post.entity(), sortDescriptors: [])
var posts: FetchedResults<Post>
var body: some View {
List {
ForEach(0..<posts.count, id: \.self) { post in
Text(self.posts[post].name ?? "?")
}
}
.navigationBarItems(trailing:
HStack(spacing: 16) {
Button(action: deletePosts) {
Image(systemName: "text.badge.minus")
}
Button(action: addPost) {
Image(systemName: "plus.app")
}
}
)
}
func addPost() {
let p = Post(context: moc)
p.name = "Post \(UUID().uuidString)"
try! moc.save()
}
func deletePosts() {
posts.forEach {
moc.delete($0)
}
try! moc.save()
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
var body: some View {
NavigationView {
VStack(alignment: .leading){
NavigationLink(destination: MessageList()) {
Text("Messages")
}.padding()
NavigationLink(destination: PostList()) {
Text("Posts")
}.padding()
Spacer()
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
static var previews: some View {
ContentView()
.environment(\.managedObjectContext, moc)
}
}
Screenshot of the Model:
The SceneDelegate (unaltered from template, provided for completeness):
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {}
func sceneDidBecomeActive(_ scene: UIScene) {}
func sceneWillResignActive(_ scene: UIScene) {}
func sceneWillEnterForeground(_ scene: UIScene) {}
func sceneDidEnterBackground(_ scene: UIScene) {
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}
}
The AppDelegate (unaltered from template, provided for completeness):
import UIKit
import CoreData
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {}
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "Coredata134")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
Update iOS 14.0 (beta 1):
This issue seems to have been resolved on iOS 14.
I also believe this is a bug.
You can workaround for now by setting the environment variable again within the NavigationLinks in ContentView:
NavigationLink(destination: MessageList().environment(\.managedObjectContext, moc)) {
Text("Messages")
}.padding()
NavigationLink(destination: PostList().environment(\.managedObjectContext, moc)) {
Text("Posts")
}.padding()
EDIT:
Just noticed that this workaround has at least one serious negative side effect: in case the #FetchRequest in the destination View uses a sortDescriptor and the destination View itself contains a NavigationLink, (e.g. to a DetailView), then modifying an attribute contained in the sortDescriptor in the DetailView will cause the DetailView to be popped and pushed again as soon as the new attribute value leads to a new sort order.
To demonstrate this:
a) add a new attribute of type Integer 16 named "value" to the Message entity in the Core Data model.
b) update func addMessage() as follows:
func addMessage() {
let m = Message(context: moc)
m.title = "Message: \(Date())"
m.value = 0
try! moc.save()
}
c) add the following struct to ContentView.swift
struct MessageDetailList: View {
#ObservedObject var message: Message
var body: some View {
Button(action: {
self.message.value += 1
}) {
Text("\(message.title ?? "?"): value = \(message.value)")
}
}
}
d) Update the ForEach in struct MessageList as follows:
ForEach(messages, id: \.self) { message in
NavigationLink(destination: MessageDetailList(message: message).environment(\.managedObjectContext, self.moc)) {
Text("\(message.title ?? "?"): value = \(message.value)")
}
}
e) replace #FetchRequest in MessageList with:
#FetchRequest(entity: Message.entity(), sortDescriptors: [NSSortDescriptor(key: "value", ascending: false)])
Run the code and tap on "Messages". Create three messages, then tap on the third one. In the DetailView, tap on the Button. This will increase the value attribute of this message to 1 and thus resort the fetch results on MessageList, which will trigger a pop and push again of the detail list.

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