Introduction
NSBatchDeleteRequest is an efficient way to delete many CoreData objects at once without loading them into memory. However, because it operates directly on the persistent store, the managed object context (MOC) is not automatically notified of the changes. This means NSFetchedResultsController does not update, existing managed objects remain in memory as faulted objects, and the UI shows stale data even though the objects have been deleted from the database.
Symptoms
- Table view still shows deleted rows after batch delete
NSFetchedResultsControllerdelegate methods not called- Fetch request returns objects that were batch deleted
- UI shows stale data until app is restarted
NSBatchDeleteRequestexecutes but no visible change
Check if delete actually worked: ```swift let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Message") let count = try context.count(for: fetchRequest) print("Messages in context: \(count)")
// Execute batch delete let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) try context.execute(deleteRequest)
// Count still returns old number - context is not updated! let countAfter = try context.count(for: fetchRequest) print("Messages after delete: \(countAfter)") // Same as before! ```
Common Causes
NSBatchDeleteRequestdoes not update the MOC automaticallyNSFetchedResultsControllernot notified of store-level changes- Deleted objects still exist in memory as managed objects
- Batch delete result not processed to merge changes
- Parent-child context setup not handling batch operations
Step-by-Step Fix
- 1.Process batch delete result to merge changes into context:
- 2.```swift
- 3.func batchDeleteMessages(olderThan date: Date, context: NSManagedObjectContext) throws {
- 4.let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Message.fetchRequest()
- 5.fetchRequest.predicate = NSPredicate(format: "createdAt < %@", date as NSDate)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
// Request object IDs of deleted objects deleteRequest.resultType = .resultTypeObjectIDs
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult guard let objectIDs = result?.result as? [NSManagedObjectID] else { return }
// Merge changes into the current context let changes = [NSDeletedObjectsKey: objectIDs] NSManagedObjectContext.mergeChanges( fromRemoteContextSave: changes, into: [context] )
// If using parent-child contexts, also merge into parent if let parent = context.parent { NSManagedObjectContext.mergeChanges( fromRemoteContextSave: changes, into: [parent] ) } } ```
- 1.**Update NSFetchedResultsController after batch delete":
- 2.```swift
- 3.class MessageListViewModel: NSObject, ObservableObject {
- 4.@Published var messages: [Message] = []
private let context: NSManagedObjectContext private var fetchController: NSFetchedResultsController<Message>
init(context: NSManagedObjectContext) { self.context = context
let fetchRequest: NSFetchRequest<Message> = Message.fetchRequest() fetchRequest.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
fetchController = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil )
super.init() fetchController.delegate = self
try? fetchController.performFetch() messages = fetchController.fetchedObjects ?? [] }
func deleteOldMessages(olderThan date: Date) { do { try batchDeleteMessages(olderThan: date, context: context) // NSFetchedResultsController delegate will be called // after mergeChanges, updating the UI automatically } catch { print("Batch delete failed: \(error)") } } }
extension MessageListViewModel: NSFetchedResultsControllerDelegate { func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { messages = controller.fetchedObjects as? [Message] ?? [] } } ```
- 1.Handle batch delete in SwiftUI with @FetchRequest:
- 2.```swift
- 3.struct MessageListView: View {
- 4.@Environment(\.managedObjectContext) private var context
- 5.@FetchRequest(
- 6.sortDescriptors: [NSSortDescriptor(keyPath: \Message.createdAt, ascending: false)],
- 7.animation: .default
- 8.) private var messages: FetchedResults<Message>
var body: some View { List { ForEach(messages) { message in MessageRow(message: message) } } .toolbar { Button("Clean Old Messages") { deleteOldMessages() } } }
private func deleteOldMessages() { let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Message.fetchRequest() let thirtyDaysAgo = Calendar.current.date(byAdding: .day, value: -30, to: Date())! fetchRequest.predicate = NSPredicate(format: "createdAt < %@", thirtyDaysAgo as NSDate)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) deleteRequest.resultType = .resultTypeObjectIDs
do { let result = try context.execute(deleteRequest) as? NSBatchDeleteResult if let objectIDs = result?.result as? [NSManagedObjectID] { // Refresh the context to update @FetchRequest let changes = [NSDeletedObjectsKey: objectIDs] NSManagedObjectContext.mergeChanges( fromRemoteContextSave: changes, into: [context] ) } } catch { print("Batch delete error: \(error)") } } } ```
Prevention
- Always set
resultType = .resultTypeObjectIDson batch delete requests - Call
NSManagedObjectContext.mergeChangesafter batch operations - Update parent contexts in parent-child MOC setups
- Test batch operations with NSFetchedResultsController to verify UI updates
- Use batch delete only for large deletions; use individual delete for small sets
- Add a post-b-delete verification fetch to confirm deletion