viewContext.execute(deleteRequest) doesn’t call redrawing SwiftUI List - core-data

I don't understand when I try to remove all items by calling viewContext.execute(deleteRequest) SwiftUI doesn't redraw UI.
I see items from sqlite are gone.
struct CloudKitTestView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
VStack {
Button("Remove all") {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try viewContext.execute(deleteRequest)
} 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)")
}
}
List {
ForEach(items) { item in
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
.onDelete(perform: deleteItems)
}
.toolbar {
#if os(iOS)
EditButton()
#endif
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
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)")
}
}
}
}

Core Data batch updates do not update the in-memory objects. You have to manually refresh afterwards.
Batch operations bypass the normal Core Data operations and operate directly on the underlying SQLite database (or whatever is backing your persistent store). They do this for benefits of speed but it means they also don't trigger all the stuff you get using normal fetch requests.
You need to do something like shown in Apple's Core Data Batch Programming Guide: Implementing Batch Updates - Updating Your Application After Execution
Original answer
do {
let fetch: NSFetchRequest<NSFetchRequestResult> = Item.fetchRequest()
let request = NSBatchDeleteRequest(fetchRequest: fetch)
request.resultType = .resultTypeObjectIDs
let result = try viewContext.execute(request) as? NSBatchDeleteResult
let objIDArray = result?.result as? [NSManagedObjectID]
let changes = [NSDeletedObjectsKey: objIDArray]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [viewContext])
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}

Related

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

Too Many Notifications CoreData/CloudKit Sync

I have several apps that use CoreData / iCloud syncing and they all receive a slew of update/change/insert/delete/etc notifications when they start and sometimes as they are running without any changes to the underlying data. When a new item is added or deleted, it appears that I get notifications for everything again. Even the number of notifications are not consistent.
My question is, how do I avoid this? Is there a cut-off that can be applied once I'm sure I have everything up to date on a device by device basis.
Persistence
import Foundation
import UIKit
import CoreData
struct PersistenceController {
let ns = NotificationStuff()
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.stuff = "Stuff"
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "TestCoreDataSync")
if inMemory {
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)")
}
})
}
}
class NotificationStuff
{
var changeCtr = 0
init()
{
NotificationCenter.default.addObserver(self, selector: #selector(self.processUpdate), name: Notification.Name.NSPersistentStoreRemoteChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contextDidSave(_:)), name: Notification.Name.NSManagedObjectContextDidSave, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contextObjectsDidChange(_:)), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
}
#objc func processUpdate(_ notification: Notification)
{
//print(notification)
DispatchQueue.main.async
{ [self] in
observerSelector(notification)
}
}
#objc func contextObjectsDidChange(_ notification: Notification)
{
DispatchQueue.main.async
{ [self] in
observerSelector(notification)
}
}
#objc func contextDidSave(_ notification: Notification)
{
DispatchQueue.main.async
{
self.observerSelector(notification)
}
}
func observerSelector(_ notification: Notification) {
DispatchQueue.main.async
{ [self] in
if let insertedObjects = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject>, !insertedObjects.isEmpty
{
print("Insert")
}
if let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject>, !updatedObjects.isEmpty
{
changeCtr = changeCtr + 1
print("Change \(changeCtr)")
}
if let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject>, !deletedObjects.isEmpty
{
print("Delete")
}
if let refreshedObjects = notification.userInfo?[NSRefreshedObjectsKey] as? Set<NSManagedObject>, !refreshedObjects.isEmpty
{
print("Refresh")
}
if let invalidatedObjects = notification.userInfo?[NSInvalidatedObjectsKey] as? Set<NSManagedObject>, !invalidatedObjects.isEmpty
{
print("Invalidate")
}
let mainManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
guard let context = notification.object as? NSManagedObjectContext else { return }
// Checks if the parent context is the main one
if context.parent === mainManagedObjectContext
{
// Saves the main context
mainManagedObjectContext.performAndWait
{
do
{
try mainManagedObjectContext.save()
} catch
{
print(error.localizedDescription)
}
}
}
}
}
}
ContentView
import SwiftUI
import CoreData
struct ContentView: View {
#State var stuff = ""
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
VStack
{
TextField("Type here", text: $stuff,onCommit: { addItem(stuff: stuff)
stuff = ""
})
List {
ForEach(items) { item in
Text(item.stuff ?? "??")
}
.onDelete(perform: deleteItems)
}
}.padding()
}
private func addItem(stuff: String) {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
newItem.stuff = stuff
do {
try viewContext.save()
} catch {
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 {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
The database has an Item entity with a timestamp field and a string field named stuff.
It depends on if it's for examining Production or Debug builds in the system's Console or Xcode's Console respectively.
For Production builds, my understanding is the aim is to make my messages more findable (rather than de-emphasising/hiding other messages) by consistently using something like:
let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "YourCategorisationOfMessagesGoingToThisHandle")
and then in the code I might have things like
log.debug("My debug message")
log.warning("My warning etc")
fwiw: I tend to categorise stuff by the file it's in, as that's deterministic and helps me find the file, so my source files tend to start with
fileprivate let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #file.components(separatedBy: "/").last ?? "")
If I do this, then I can easily filter the system's console messages to find stuff that's relevant to my app.
There's more on how to use this and the console to filter for the app's messages in the sytem console over here.
For Debug builds and the Xcode console the same consistent app log messages from my app could be used, e.g. my app's debug messages always start with "Some easily findable string or other". I don't believe there is a way to throttle/cut-off responses selectively. But it definitely possible to turn off debug messages from many of the noisy sub-systems completely (once happy that they are working reliably)
For Core Data and CloudKit cases mentioned, if I run the Debug builds with the -com.apple.CoreData.Logging.stderr 0 and -com.apple.CoreData.CloudKitDebug 0 launch args then that make Xcode's console a lot quieter :-). Nice instructions on how to set this up in the SO answer over here
My problem was that CoreData -> CloudKit integration was re-synching the same items over and over, thus the notifications. I discovered that I needed to add a sorted index for the modifiedTimestamp on all entities. Now things are much faster and few if any re-synched items.

How do I get SwiftUI to work with Core Data (after starting a project)?

I get this error when trying to get Xcode working with Core Data. Any help would be very appreciated.
Context in environment is not connected to a persistent store coordinator: <NSManagedObjectContext: 0x6000008a8820>
After commenting out all the other environments so that the persistence envinronment is only used in the top level, I still have the same error.
Here is the Apps Top-Level Swift File:
import SwiftUI
import CoreData
#main
struct Draw_DailyApp: App {
let persistenceController = PersistenceController.shared
#Environment(\.scenePhase) var scenePhase
#FetchRequest(entity: Drawing.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Drawing.img, ascending: true)])
var orders: FetchedResults<Drawing>
#State var showOrderSheet = false
// Creating a global environment and placing it into the environment
#ObservedObject var searchObjectController: SearchObjectController = SearchObjectController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(self.searchObjectController)
}
.onChange(of: scenePhase) { _ in
persistenceController.save()
}
}
}
and here is the file where persistent controller is used (Core Data xcdatamodeld has one Binary Data attribute called img)
Persistence.Swift
import CoreData
import UIKit
struct PersistenceController {
static let shared = PersistenceController()
func save() {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Show some error here
}
}
}
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Drawing(context: viewContext)
newItem.img = Data()
newItem.date = "02081999"
}
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)")
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "DrawDaily")
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)")
}
})
}
}
PS: I didn't start this project with Core Data, I added it in later
Your #FetchRequest doesn't have a NSManagedObjectContext upon init. You are creating it with this line
let persistenceController = PersistenceController.shared
Your #FetchRequest has to move down into the ContentView for it to work.
This line has to be called
.environment(\.managedObjectContext, persistenceController.container.viewContext)
when initializing the View that contains the #FetchRequest

SwiftUI CloudKit Public Database with NSPersistentCloudKitContainer

Based on WWDC20 talk bellow:
https://developer.apple.com/videos/play/wwdc2020/10650/
The way to setup CloudKit Public Database with NSPersistentCloudKitContainer in "one line of code" is this:
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey:NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.cloudKitContainerOptions?.databaseScope = .public
How would that be on the new SwiftUI Persistent.swift template?
I tried the code bellow but didn't work:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
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)")
}
return result
}()
let container: NSPersistentCloudKitContainer
//This doesnt work
//container.cloudKitContainerOptions?.databaseScope = .public
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Market")
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)")
}
})
}
}
guard let description = container.persistentStoreDescriptions.first else {
print("Can't set description")
fatalError("Error")
}
description.cloudKitContainerOptions?.databaseScope = .public

SwiftUI - Pass a fetchResult to another View

I'm trying to pass a FetchResult to another view in order to have all my tables updated at the same time.
My problem:
view1 {
#FetchRequest
ForEach{
NavigationLink(passing fetchRequest.value to View 2)}
}
View2 {
var value1 :fetchRequest.value from view 1
ForEach{
NavigationLink(passing value1.value to View 3)}
}
View3....
Problem here is, if I do a delete or a add on the view 3, the views 1 and 2 won't update until I go back to view 1, and descend again to view 2 and 3.
Do you have an idea on how to have a quick update of these values ?
Best
Tim
I've never seen anyone else try this before but I just lifted the #FetchRequest up into a superview and passed the fetch results (items in this case) as a param down to the subview:
struct ContentView: View {
#State var count = 0
#FetchRequest<Item>(sortDescriptors: [], predicate: nil, animation: nil) var items
var body: some View {
NavigationView {
MasterView(items: items)
.navigationTitle("Master \(count)")
.navigationBarItems(trailing: Button("Increment"){
count += 1
})
}
}
}
struct MasterView: View {
var items : FetchedResults<Item>
var body: some View {
List {
ForEach(items) { item in
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
.onDelete(perform: deleteItems)
}
.toolbar {
// #if os(iOS)
// ToolbarItem(placement: .navigation){
// EditButton()
// }
// #endif
//ToolbarItem(placement: .automatic){
ToolbarItem(placement: .bottomBar){
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
ToolbarItem(placement: .bottomBar){
Button(action: {
ascending.toggle()
}) {
Text(ascending ? "Descending" : "Ascending")
}
}
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
newItem.name = "Master"
do {
try newItem.validateForInsert()
try viewContext.obtainPermanentIDs(for: [newItem])
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)")
}
}
}
}
The reason I did this is I used a the launch argument -com.apple.CoreData.SQLDebug 4 and I noticed it was hitting the database every time the state changed and a View was recreated that contained a #FetchRequest which I didn't want.

Resources