Saving in Core Data
Saving in Core Data
Section titled “Saving in Core Data”Saving data efficiently is crucial for app performance and user experience. This guide covers best practices for when, how, and where to save your Core Data changes.
The Problem with Always Saving
Section titled “The Problem with Always Saving”Calling save() unconditionally has performance costs:
// ❌ Bad: Always saving, even when nothing changedfunc updateUI() { article.lastViewed = Date() try? context.save() // Expensive even if nothing changed!}Problems:
- Writes to disk even when no changes exist
- Triggers merge notifications unnecessarily
- Wastes CPU and battery
- Slows down your app
Conditional Saving with hasChanges
Section titled “Conditional Saving with hasChanges”The first improvement is checking hasChanges:
// ✅ Better: Only save if there are changesif context.hasChanges { try context.save()}Benefits:
- Avoids unnecessary disk writes
- Faster performance
- Still simple to use
Limitation:
hasChangesreturnstruefor transient properties too- Transient changes don’t need to be persisted
Best Practice: hasPersistentChanges
Section titled “Best Practice: hasPersistentChanges”Check for persistent changes only, excluding transient properties:
extension NSManagedObjectContext { var hasPersistentChanges: Bool { return !insertedObjects.isEmpty || !deletedObjects.isEmpty || updatedObjects.contains(where: { $0.hasPersistentChangedValues }) }
func saveIfNeeded() throws { guard hasPersistentChanges else { return } try save() }}
// Usagetry context.saveIfNeeded()Why this is better:
- Excludes transient property changes
- Only saves when data actually needs persisting
- Most efficient approach
Understanding hasPersistentChangedValues
Section titled “Understanding hasPersistentChangedValues”extension NSManagedObject { var hasPersistentChangedValues: Bool { return !changedValues().isEmpty }}This checks if the object has any changed values. For more granular control:
extension NSManagedObject { var hasPersistentChangedValues: Bool { let changedKeys = Set(changedValues().keys) let persistentKeys = Set(entity.attributesByName.keys) .union(entity.relationshipsByName.keys) .subtracting(entity.transientAttributeNames) return !changedKeys.intersection(persistentKeys).isEmpty }}
extension NSEntityDescription { var transientAttributeNames: Set<String> { return Set(attributesByName.filter { $0.value.isTransient }.map { $0.key }) }}When to Save
Section titled “When to Save”Save on App Lifecycle Events
Section titled “Save on App Lifecycle Events”// AppDelegate or SceneDelegatefunc applicationWillTerminate(_ application: UIApplication) { try? CoreDataStack.shared.viewContext.saveIfNeeded()}
func sceneDidEnterBackground(_ scene: UIScene) { try? CoreDataStack.shared.viewContext.saveIfNeeded()}Save After User Actions
Section titled “Save After User Actions”// After user completes an action@IBAction func saveButtonTapped(_ sender: UIButton) { article.name = nameTextField.text article.content = contentTextView.text
do { try context.saveIfNeeded() dismiss(animated: true) } catch { // Handle error showError(error) }}Save Periodically for Long-Running Operations
Section titled “Save Periodically for Long-Running Operations”func importLargeDataset() { let context = container.newBackgroundContext() context.perform { for (index, data) in largeDataset.enumerated() { let article = Article(context: context) article.name = data.name
// Save every 100 objects if index % 100 == 0 { try? context.saveIfNeeded() } }
// Final save try? context.saveIfNeeded() }}Don’t Save Too Frequently
Section titled “Don’t Save Too Frequently”// ❌ Bad: Saving on every keystrokefunc textFieldDidChange(_ textField: UITextField) { article.name = textField.text try? context.save() // Too frequent!}
// ✅ Better: Save when editing endsfunc textFieldDidEndEditing(_ textField: UITextField) { article.name = textField.text try? context.saveIfNeeded()}
// ✅ Best: Use debouncing for auto-saveprivate var saveWorkItem: DispatchWorkItem?
func textFieldDidChange(_ textField: UITextField) { article.name = textField.text
// Cancel previous save saveWorkItem?.cancel()
// Schedule new save after 2 seconds of inactivity let workItem = DispatchWorkItem { [weak self] in try? self?.context.saveIfNeeded() } saveWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: workItem)}Error Handling
Section titled “Error Handling”Basic Error Handling
Section titled “Basic Error Handling”do { try context.save()} catch { print("Failed to save: \(error)")}Detailed Error Handling
Section titled “Detailed Error Handling”do { try context.save()} catch let error as NSError { print("Failed to save context: \(error)") print("User info: \(error.userInfo)")
// Check for specific errors if error.domain == NSCocoaErrorDomain { switch error.code { case NSValidationStringTooShortError: print("String too short") case NSValidationStringTooLongError: print("String too long") case NSManagedObjectValidationError: print("Validation error") case NSManagedObjectConstraintValidationError: print("Constraint violation") default: print("Other error: \(error.code)") } }}User-Friendly Error Messages
Section titled “User-Friendly Error Messages”extension NSError { var userFriendlyMessage: String { guard domain == NSCocoaErrorDomain else { return localizedDescription }
switch code { case NSValidationStringTooShortError: return "The text is too short. Please enter at least 3 characters." case NSValidationStringTooLongError: return "The text is too long. Please keep it under 100 characters." case NSManagedObjectConstraintValidationError: return "This item already exists. Please use a different name." case NSManagedObjectValidationError: return "Please check your input and try again." default: return "Failed to save: \(localizedDescription)" } }}
// Usagedo { try context.save()} catch let error as NSError { showAlert(message: error.userFriendlyMessage)}Saving in Different Contexts
Section titled “Saving in Different Contexts”View Context (Main Thread)
Section titled “View Context (Main Thread)”// Always on main threadlet context = container.viewContext
// Simple savetry? context.saveIfNeeded()
// With error handlingdo { try context.saveIfNeeded()} catch { print("Failed to save: \(error)")}Background Context
Section titled “Background Context”let context = container.newBackgroundContext()context.perform { // Make changes let article = Article(context: context) article.name = "New Article"
// Save within perform block do { try context.saveIfNeeded() } catch { print("Failed to save: \(error)") }}Nested Contexts (Advanced)
Section titled “Nested Contexts (Advanced)”// Parent context (view context)let parentContext = container.viewContext
// Child context for editinglet childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)childContext.parent = parentContext
// Make changes in childlet article = childContext.object(with: articleID) as! Articlearticle.name = "Updated"
// Save child (pushes to parent, not to disk)try? childContext.save()
// Save parent to persist to disktry? parentContext.save()Use nested contexts for:
- Cancellable editing (discard child without saving parent)
- Temporary changes
- Complex forms
Saving with Validation
Section titled “Saving with Validation”Core Data validates objects before saving. Handle validation errors appropriately:
do { try context.save()} catch let error as NSError { if error.code == NSValidationMultipleErrorsError { // Multiple validation errors if let errors = error.userInfo[NSDetailedErrorsKey] as? [NSError] { for validationError in errors { print("Validation error: \(validationError.localizedDescription)")
// Get the object that failed validation if let object = validationError.userInfo[NSValidationObjectErrorKey] as? NSManagedObject { print("Failed object: \(object)") }
// Get the property that failed if let key = validationError.userInfo[NSValidationKeyErrorKey] as? String { print("Failed property: \(key)") } } } }}Optimizing Save Performance
Section titled “Optimizing Save Performance”Batch Saves During Import
Section titled “Batch Saves During Import”func importArticles(_ articles: [ArticleData]) { let context = container.newBackgroundContext() context.perform { for (index, data) in articles.enumerated() { let article = Article(context: context) article.name = data.name article.content = data.content
// Save every 100 objects to avoid memory buildup if index % 100 == 0 && context.hasChanges { try? context.save() context.reset() // Clear memory } }
// Final save try? context.save() }}Avoid Saving in Loops
Section titled “Avoid Saving in Loops”// ❌ Bad: Saving inside loopfor data in dataArray { let article = Article(context: context) article.name = data.name try? context.save() // Very slow!}
// ✅ Good: Save once after loopfor data in dataArray { let article = Article(context: context) article.name = data.name}try? context.save() // Much faster!Use Batch Operations for Bulk Changes
Section titled “Use Batch Operations for Bulk Changes”For large-scale operations, use batch requests instead of saving individual objects:
// Instead of:for article in articles { article.isRead = true}try? context.save()
// Use batch update:let batchUpdate = NSBatchUpdateRequest(entityName: "Article")batchUpdate.predicate = NSPredicate(format: "isRead == NO")batchUpdate.propertiesToUpdate = ["isRead": true]try? context.execute(batchUpdate)See batch-operations.md for more details.
Checking for Unsaved Changes
Section titled “Checking for Unsaved Changes”Before Dismissing a View
Section titled “Before Dismissing a View”func dismiss() { if context.hasChanges { let alert = UIAlertController( title: "Unsaved Changes", message: "Do you want to save your changes?", preferredStyle: .alert )
alert.addAction(UIAlertAction(title: "Save", style: .default) { _ in try? self.context.save() self.dismissView() })
alert.addAction(UIAlertAction(title: "Discard", style: .destructive) { _ in self.context.rollback() self.dismissView() })
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true) } else { dismissView() }}Rollback Unsaved Changes
Section titled “Rollback Unsaved Changes”// Discard all unsaved changescontext.rollback()
// Refresh a specific object to discard its changescontext.refresh(article, mergeChanges: false)Save Notifications
Section titled “Save Notifications”Observe save notifications to respond to changes:
NotificationCenter.default.addObserver( self, selector: #selector(contextDidSave), name: .NSManagedObjectContextDidSave, object: context)
@objc func contextDidSave(_ notification: Notification) { guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject> { print("Inserted: \(inserts.count) objects") }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject> { print("Updated: \(updates.count) objects") }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject> { print("Deleted: \(deletes.count) objects") }}Testing Saves
Section titled “Testing Saves”In Unit Tests
Section titled “In Unit Tests”func testSaveArticle() throws { let context = testContainer.viewContext
let article = Article(context: context) article.name = "Test Article"
XCTAssertTrue(context.hasChanges)
try context.save()
XCTAssertFalse(context.hasChanges)
// Verify saved let fetchRequest = Article.fetchRequest() let results = try context.fetch(fetchRequest) XCTAssertEqual(results.count, 1) XCTAssertEqual(results.first?.name, "Test Article")}Common Pitfalls
Section titled “Common Pitfalls”❌ Not Checking for Changes
Section titled “❌ Not Checking for Changes”// Wastes resourcestry? context.save()❌ Saving Too Frequently
Section titled “❌ Saving Too Frequently”// In a loop - very slowfor item in items { item.processed = true try? context.save()}❌ Ignoring Errors
Section titled “❌ Ignoring Errors”// Silent failurestry? context.save()❌ Saving on Wrong Thread
Section titled “❌ Saving on Wrong Thread”// Crash! Background context on main threadlet context = container.newBackgroundContext()try? context.save() // Not in perform block!✅ Correct Approach
Section titled “✅ Correct Approach”// Check for changesguard context.hasPersistentChanges else { return }
// Handle errorsdo { try context.save()} catch { print("Save failed: \(error)") // Handle appropriately}
// Use correct threadcontext.perform { try? context.save()}Summary
Section titled “Summary”- Use
saveIfNeeded()withhasPersistentChanges- Most efficient approach - Save at appropriate times - App lifecycle events, after user actions, periodically
- Don’t save too frequently - Debounce auto-saves, avoid saving in loops
- Handle errors properly - Don’t ignore save failures
- Use correct context type - View context for UI, background for heavy work
- Always use
performwith background contexts - Thread safety - Consider batch operations - For large-scale updates
- Test your saves - Verify data persists correctly