opencodekit 0.16.15 → 0.16.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -242
- package/dist/index.js +19 -6
- package/dist/template/.opencode/AGENTS.md +72 -236
- package/dist/template/.opencode/README.md +49 -482
- package/dist/template/.opencode/agent/build.md +71 -345
- package/dist/template/.opencode/agent/explore.md +47 -139
- package/dist/template/.opencode/agent/general.md +61 -172
- package/dist/template/.opencode/agent/looker.md +65 -161
- package/dist/template/.opencode/agent/painter.md +46 -200
- package/dist/template/.opencode/agent/plan.md +37 -220
- package/dist/template/.opencode/agent/review.md +72 -153
- package/dist/template/.opencode/agent/scout.md +44 -486
- package/dist/template/.opencode/agent/vision.md +63 -178
- package/dist/template/.opencode/command/create.md +75 -307
- package/dist/template/.opencode/command/design.md +53 -589
- package/dist/template/.opencode/command/handoff.md +76 -180
- package/dist/template/.opencode/command/init.md +45 -211
- package/dist/template/.opencode/command/plan.md +62 -514
- package/dist/template/.opencode/command/pr.md +56 -226
- package/dist/template/.opencode/command/research.md +55 -266
- package/dist/template/.opencode/command/resume.md +33 -138
- package/dist/template/.opencode/command/review-codebase.md +54 -202
- package/dist/template/.opencode/command/ship.md +78 -127
- package/dist/template/.opencode/command/start.md +47 -577
- package/dist/template/.opencode/command/status.md +60 -353
- package/dist/template/.opencode/command/ui-review.md +52 -298
- package/dist/template/.opencode/command/verify.md +36 -250
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +133 -35
- package/dist/template/.opencode/plugin/README.md +40 -166
- package/dist/template/.opencode/plugin/compaction.ts +162 -131
- package/dist/template/.opencode/plugin/lib/memory-db.ts +112 -0
- package/dist/template/.opencode/plugin/swarm-enforcer.ts +182 -27
- package/dist/template/.opencode/skill/augment-context-engine/SKILL.md +112 -0
- package/dist/template/.opencode/skill/augment-context-engine/mcp.json +6 -0
- package/dist/template/.opencode/skill/core-data-expert/SKILL.md +82 -0
- package/dist/template/.opencode/skill/core-data-expert/references/batch-operations.md +543 -0
- package/dist/template/.opencode/skill/core-data-expert/references/cloudkit-integration.md +259 -0
- package/dist/template/.opencode/skill/core-data-expert/references/concurrency.md +522 -0
- package/dist/template/.opencode/skill/core-data-expert/references/fetch-requests.md +643 -0
- package/dist/template/.opencode/skill/core-data-expert/references/glossary.md +233 -0
- package/dist/template/.opencode/skill/core-data-expert/references/migration.md +393 -0
- package/dist/template/.opencode/skill/core-data-expert/references/model-configuration.md +597 -0
- package/dist/template/.opencode/skill/core-data-expert/references/performance.md +300 -0
- package/dist/template/.opencode/skill/core-data-expert/references/persistent-history.md +553 -0
- package/dist/template/.opencode/skill/core-data-expert/references/project-audit.md +60 -0
- package/dist/template/.opencode/skill/core-data-expert/references/saving.md +574 -0
- package/dist/template/.opencode/skill/core-data-expert/references/stack-setup.md +625 -0
- package/dist/template/.opencode/skill/core-data-expert/references/testing.md +300 -0
- package/dist/template/.opencode/skill/core-data-expert/references/threading.md +589 -0
- package/dist/template/.opencode/skill/swift-concurrency/SKILL.md +246 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/actors.md +640 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-algorithms.md +822 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-await-basics.md +249 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-sequences.md +670 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/core-data.md +533 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/glossary.md +128 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/linting.md +142 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/memory-management.md +542 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/migration.md +1076 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/performance.md +574 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/sendable.md +578 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/tasks.md +604 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/testing.md +565 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/threading.md +452 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/SKILL.md +290 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-advanced.md +351 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/image-optimization.md +286 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/layout-best-practices.md +312 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/liquid-glass.md +377 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/list-patterns.md +153 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/modern-apis.md +400 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/performance-patterns.md +377 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/scroll-patterns.md +305 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/sheet-navigation-patterns.md +292 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/state-management.md +447 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/text-formatting.md +285 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/view-structure.md +276 -0
- package/dist/template/.opencode/tool/action-queue.ts +308 -0
- package/dist/template/.opencode/tool/swarm.ts +65 -40
- package/package.json +16 -3
- package/dist/template/.opencode/.agents/skills/context7/SKILL.md +0 -88
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
# Saving in Core Data
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## The Problem with Always Saving
|
|
6
|
+
|
|
7
|
+
Calling `save()` unconditionally has performance costs:
|
|
8
|
+
|
|
9
|
+
```swift
|
|
10
|
+
// ❌ Bad: Always saving, even when nothing changed
|
|
11
|
+
func updateUI() {
|
|
12
|
+
article.lastViewed = Date()
|
|
13
|
+
try? context.save() // Expensive even if nothing changed!
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Problems:**
|
|
18
|
+
- Writes to disk even when no changes exist
|
|
19
|
+
- Triggers merge notifications unnecessarily
|
|
20
|
+
- Wastes CPU and battery
|
|
21
|
+
- Slows down your app
|
|
22
|
+
|
|
23
|
+
## Conditional Saving with hasChanges
|
|
24
|
+
|
|
25
|
+
The first improvement is checking `hasChanges`:
|
|
26
|
+
|
|
27
|
+
```swift
|
|
28
|
+
// ✅ Better: Only save if there are changes
|
|
29
|
+
if context.hasChanges {
|
|
30
|
+
try context.save()
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Benefits:**
|
|
35
|
+
- Avoids unnecessary disk writes
|
|
36
|
+
- Faster performance
|
|
37
|
+
- Still simple to use
|
|
38
|
+
|
|
39
|
+
**Limitation:**
|
|
40
|
+
- `hasChanges` returns `true` for transient properties too
|
|
41
|
+
- Transient changes don't need to be persisted
|
|
42
|
+
|
|
43
|
+
## Best Practice: hasPersistentChanges
|
|
44
|
+
|
|
45
|
+
Check for **persistent** changes only, excluding transient properties:
|
|
46
|
+
|
|
47
|
+
```swift
|
|
48
|
+
extension NSManagedObjectContext {
|
|
49
|
+
var hasPersistentChanges: Bool {
|
|
50
|
+
return !insertedObjects.isEmpty ||
|
|
51
|
+
!deletedObjects.isEmpty ||
|
|
52
|
+
updatedObjects.contains(where: { $0.hasPersistentChangedValues })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func saveIfNeeded() throws {
|
|
56
|
+
guard hasPersistentChanges else { return }
|
|
57
|
+
try save()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Usage
|
|
62
|
+
try context.saveIfNeeded()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Why this is better:**
|
|
66
|
+
- Excludes transient property changes
|
|
67
|
+
- Only saves when data actually needs persisting
|
|
68
|
+
- Most efficient approach
|
|
69
|
+
|
|
70
|
+
### Understanding hasPersistentChangedValues
|
|
71
|
+
|
|
72
|
+
```swift
|
|
73
|
+
extension NSManagedObject {
|
|
74
|
+
var hasPersistentChangedValues: Bool {
|
|
75
|
+
return !changedValues().isEmpty
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This checks if the object has **any** changed values. For more granular control:
|
|
81
|
+
|
|
82
|
+
```swift
|
|
83
|
+
extension NSManagedObject {
|
|
84
|
+
var hasPersistentChangedValues: Bool {
|
|
85
|
+
let changedKeys = Set(changedValues().keys)
|
|
86
|
+
let persistentKeys = Set(entity.attributesByName.keys)
|
|
87
|
+
.union(entity.relationshipsByName.keys)
|
|
88
|
+
.subtracting(entity.transientAttributeNames)
|
|
89
|
+
return !changedKeys.intersection(persistentKeys).isEmpty
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
extension NSEntityDescription {
|
|
94
|
+
var transientAttributeNames: Set<String> {
|
|
95
|
+
return Set(attributesByName.filter { $0.value.isTransient }.map { $0.key })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## When to Save
|
|
101
|
+
|
|
102
|
+
### Save on App Lifecycle Events
|
|
103
|
+
|
|
104
|
+
```swift
|
|
105
|
+
// AppDelegate or SceneDelegate
|
|
106
|
+
func applicationWillTerminate(_ application: UIApplication) {
|
|
107
|
+
try? CoreDataStack.shared.viewContext.saveIfNeeded()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func sceneDidEnterBackground(_ scene: UIScene) {
|
|
111
|
+
try? CoreDataStack.shared.viewContext.saveIfNeeded()
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Save After User Actions
|
|
116
|
+
|
|
117
|
+
```swift
|
|
118
|
+
// After user completes an action
|
|
119
|
+
@IBAction func saveButtonTapped(_ sender: UIButton) {
|
|
120
|
+
article.name = nameTextField.text
|
|
121
|
+
article.content = contentTextView.text
|
|
122
|
+
|
|
123
|
+
do {
|
|
124
|
+
try context.saveIfNeeded()
|
|
125
|
+
dismiss(animated: true)
|
|
126
|
+
} catch {
|
|
127
|
+
// Handle error
|
|
128
|
+
showError(error)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Save Periodically for Long-Running Operations
|
|
134
|
+
|
|
135
|
+
```swift
|
|
136
|
+
func importLargeDataset() {
|
|
137
|
+
let context = container.newBackgroundContext()
|
|
138
|
+
context.perform {
|
|
139
|
+
for (index, data) in largeDataset.enumerated() {
|
|
140
|
+
let article = Article(context: context)
|
|
141
|
+
article.name = data.name
|
|
142
|
+
|
|
143
|
+
// Save every 100 objects
|
|
144
|
+
if index % 100 == 0 {
|
|
145
|
+
try? context.saveIfNeeded()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Final save
|
|
150
|
+
try? context.saveIfNeeded()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Don't Save Too Frequently
|
|
156
|
+
|
|
157
|
+
```swift
|
|
158
|
+
// ❌ Bad: Saving on every keystroke
|
|
159
|
+
func textFieldDidChange(_ textField: UITextField) {
|
|
160
|
+
article.name = textField.text
|
|
161
|
+
try? context.save() // Too frequent!
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ✅ Better: Save when editing ends
|
|
165
|
+
func textFieldDidEndEditing(_ textField: UITextField) {
|
|
166
|
+
article.name = textField.text
|
|
167
|
+
try? context.saveIfNeeded()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ✅ Best: Use debouncing for auto-save
|
|
171
|
+
private var saveWorkItem: DispatchWorkItem?
|
|
172
|
+
|
|
173
|
+
func textFieldDidChange(_ textField: UITextField) {
|
|
174
|
+
article.name = textField.text
|
|
175
|
+
|
|
176
|
+
// Cancel previous save
|
|
177
|
+
saveWorkItem?.cancel()
|
|
178
|
+
|
|
179
|
+
// Schedule new save after 2 seconds of inactivity
|
|
180
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
181
|
+
try? self?.context.saveIfNeeded()
|
|
182
|
+
}
|
|
183
|
+
saveWorkItem = workItem
|
|
184
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: workItem)
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Error Handling
|
|
189
|
+
|
|
190
|
+
### Basic Error Handling
|
|
191
|
+
|
|
192
|
+
```swift
|
|
193
|
+
do {
|
|
194
|
+
try context.save()
|
|
195
|
+
} catch {
|
|
196
|
+
print("Failed to save: \(error)")
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Detailed Error Handling
|
|
201
|
+
|
|
202
|
+
```swift
|
|
203
|
+
do {
|
|
204
|
+
try context.save()
|
|
205
|
+
} catch let error as NSError {
|
|
206
|
+
print("Failed to save context: \(error)")
|
|
207
|
+
print("User info: \(error.userInfo)")
|
|
208
|
+
|
|
209
|
+
// Check for specific errors
|
|
210
|
+
if error.domain == NSCocoaErrorDomain {
|
|
211
|
+
switch error.code {
|
|
212
|
+
case NSValidationStringTooShortError:
|
|
213
|
+
print("String too short")
|
|
214
|
+
case NSValidationStringTooLongError:
|
|
215
|
+
print("String too long")
|
|
216
|
+
case NSManagedObjectValidationError:
|
|
217
|
+
print("Validation error")
|
|
218
|
+
case NSManagedObjectConstraintValidationError:
|
|
219
|
+
print("Constraint violation")
|
|
220
|
+
default:
|
|
221
|
+
print("Other error: \(error.code)")
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### User-Friendly Error Messages
|
|
228
|
+
|
|
229
|
+
```swift
|
|
230
|
+
extension NSError {
|
|
231
|
+
var userFriendlyMessage: String {
|
|
232
|
+
guard domain == NSCocoaErrorDomain else {
|
|
233
|
+
return localizedDescription
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
switch code {
|
|
237
|
+
case NSValidationStringTooShortError:
|
|
238
|
+
return "The text is too short. Please enter at least 3 characters."
|
|
239
|
+
case NSValidationStringTooLongError:
|
|
240
|
+
return "The text is too long. Please keep it under 100 characters."
|
|
241
|
+
case NSManagedObjectConstraintValidationError:
|
|
242
|
+
return "This item already exists. Please use a different name."
|
|
243
|
+
case NSManagedObjectValidationError:
|
|
244
|
+
return "Please check your input and try again."
|
|
245
|
+
default:
|
|
246
|
+
return "Failed to save: \(localizedDescription)"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Usage
|
|
252
|
+
do {
|
|
253
|
+
try context.save()
|
|
254
|
+
} catch let error as NSError {
|
|
255
|
+
showAlert(message: error.userFriendlyMessage)
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Saving in Different Contexts
|
|
260
|
+
|
|
261
|
+
### View Context (Main Thread)
|
|
262
|
+
|
|
263
|
+
```swift
|
|
264
|
+
// Always on main thread
|
|
265
|
+
let context = container.viewContext
|
|
266
|
+
|
|
267
|
+
// Simple save
|
|
268
|
+
try? context.saveIfNeeded()
|
|
269
|
+
|
|
270
|
+
// With error handling
|
|
271
|
+
do {
|
|
272
|
+
try context.saveIfNeeded()
|
|
273
|
+
} catch {
|
|
274
|
+
print("Failed to save: \(error)")
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Background Context
|
|
279
|
+
|
|
280
|
+
```swift
|
|
281
|
+
let context = container.newBackgroundContext()
|
|
282
|
+
context.perform {
|
|
283
|
+
// Make changes
|
|
284
|
+
let article = Article(context: context)
|
|
285
|
+
article.name = "New Article"
|
|
286
|
+
|
|
287
|
+
// Save within perform block
|
|
288
|
+
do {
|
|
289
|
+
try context.saveIfNeeded()
|
|
290
|
+
} catch {
|
|
291
|
+
print("Failed to save: \(error)")
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Nested Contexts (Advanced)
|
|
297
|
+
|
|
298
|
+
```swift
|
|
299
|
+
// Parent context (view context)
|
|
300
|
+
let parentContext = container.viewContext
|
|
301
|
+
|
|
302
|
+
// Child context for editing
|
|
303
|
+
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
|
|
304
|
+
childContext.parent = parentContext
|
|
305
|
+
|
|
306
|
+
// Make changes in child
|
|
307
|
+
let article = childContext.object(with: articleID) as! Article
|
|
308
|
+
article.name = "Updated"
|
|
309
|
+
|
|
310
|
+
// Save child (pushes to parent, not to disk)
|
|
311
|
+
try? childContext.save()
|
|
312
|
+
|
|
313
|
+
// Save parent to persist to disk
|
|
314
|
+
try? parentContext.save()
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Use nested contexts for:**
|
|
318
|
+
- Cancellable editing (discard child without saving parent)
|
|
319
|
+
- Temporary changes
|
|
320
|
+
- Complex forms
|
|
321
|
+
|
|
322
|
+
## Saving with Validation
|
|
323
|
+
|
|
324
|
+
Core Data validates objects before saving. Handle validation errors appropriately:
|
|
325
|
+
|
|
326
|
+
```swift
|
|
327
|
+
do {
|
|
328
|
+
try context.save()
|
|
329
|
+
} catch let error as NSError {
|
|
330
|
+
if error.code == NSValidationMultipleErrorsError {
|
|
331
|
+
// Multiple validation errors
|
|
332
|
+
if let errors = error.userInfo[NSDetailedErrorsKey] as? [NSError] {
|
|
333
|
+
for validationError in errors {
|
|
334
|
+
print("Validation error: \(validationError.localizedDescription)")
|
|
335
|
+
|
|
336
|
+
// Get the object that failed validation
|
|
337
|
+
if let object = validationError.userInfo[NSValidationObjectErrorKey] as? NSManagedObject {
|
|
338
|
+
print("Failed object: \(object)")
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Get the property that failed
|
|
342
|
+
if let key = validationError.userInfo[NSValidationKeyErrorKey] as? String {
|
|
343
|
+
print("Failed property: \(key)")
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Optimizing Save Performance
|
|
352
|
+
|
|
353
|
+
### Batch Saves During Import
|
|
354
|
+
|
|
355
|
+
```swift
|
|
356
|
+
func importArticles(_ articles: [ArticleData]) {
|
|
357
|
+
let context = container.newBackgroundContext()
|
|
358
|
+
context.perform {
|
|
359
|
+
for (index, data) in articles.enumerated() {
|
|
360
|
+
let article = Article(context: context)
|
|
361
|
+
article.name = data.name
|
|
362
|
+
article.content = data.content
|
|
363
|
+
|
|
364
|
+
// Save every 100 objects to avoid memory buildup
|
|
365
|
+
if index % 100 == 0 && context.hasChanges {
|
|
366
|
+
try? context.save()
|
|
367
|
+
context.reset() // Clear memory
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Final save
|
|
372
|
+
try? context.save()
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Avoid Saving in Loops
|
|
378
|
+
|
|
379
|
+
```swift
|
|
380
|
+
// ❌ Bad: Saving inside loop
|
|
381
|
+
for data in dataArray {
|
|
382
|
+
let article = Article(context: context)
|
|
383
|
+
article.name = data.name
|
|
384
|
+
try? context.save() // Very slow!
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ✅ Good: Save once after loop
|
|
388
|
+
for data in dataArray {
|
|
389
|
+
let article = Article(context: context)
|
|
390
|
+
article.name = data.name
|
|
391
|
+
}
|
|
392
|
+
try? context.save() // Much faster!
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Use Batch Operations for Bulk Changes
|
|
396
|
+
|
|
397
|
+
For large-scale operations, use batch requests instead of saving individual objects:
|
|
398
|
+
|
|
399
|
+
```swift
|
|
400
|
+
// Instead of:
|
|
401
|
+
for article in articles {
|
|
402
|
+
article.isRead = true
|
|
403
|
+
}
|
|
404
|
+
try? context.save()
|
|
405
|
+
|
|
406
|
+
// Use batch update:
|
|
407
|
+
let batchUpdate = NSBatchUpdateRequest(entityName: "Article")
|
|
408
|
+
batchUpdate.predicate = NSPredicate(format: "isRead == NO")
|
|
409
|
+
batchUpdate.propertiesToUpdate = ["isRead": true]
|
|
410
|
+
try? context.execute(batchUpdate)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
See `batch-operations.md` for more details.
|
|
414
|
+
|
|
415
|
+
## Checking for Unsaved Changes
|
|
416
|
+
|
|
417
|
+
### Before Dismissing a View
|
|
418
|
+
|
|
419
|
+
```swift
|
|
420
|
+
func dismiss() {
|
|
421
|
+
if context.hasChanges {
|
|
422
|
+
let alert = UIAlertController(
|
|
423
|
+
title: "Unsaved Changes",
|
|
424
|
+
message: "Do you want to save your changes?",
|
|
425
|
+
preferredStyle: .alert
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
alert.addAction(UIAlertAction(title: "Save", style: .default) { _ in
|
|
429
|
+
try? self.context.save()
|
|
430
|
+
self.dismissView()
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
alert.addAction(UIAlertAction(title: "Discard", style: .destructive) { _ in
|
|
434
|
+
self.context.rollback()
|
|
435
|
+
self.dismissView()
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
439
|
+
|
|
440
|
+
present(alert, animated: true)
|
|
441
|
+
} else {
|
|
442
|
+
dismissView()
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Rollback Unsaved Changes
|
|
448
|
+
|
|
449
|
+
```swift
|
|
450
|
+
// Discard all unsaved changes
|
|
451
|
+
context.rollback()
|
|
452
|
+
|
|
453
|
+
// Refresh a specific object to discard its changes
|
|
454
|
+
context.refresh(article, mergeChanges: false)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Save Notifications
|
|
458
|
+
|
|
459
|
+
Observe save notifications to respond to changes:
|
|
460
|
+
|
|
461
|
+
```swift
|
|
462
|
+
NotificationCenter.default.addObserver(
|
|
463
|
+
self,
|
|
464
|
+
selector: #selector(contextDidSave),
|
|
465
|
+
name: .NSManagedObjectContextDidSave,
|
|
466
|
+
object: context
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
@objc func contextDidSave(_ notification: Notification) {
|
|
470
|
+
guard let userInfo = notification.userInfo else { return }
|
|
471
|
+
|
|
472
|
+
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject> {
|
|
473
|
+
print("Inserted: \(inserts.count) objects")
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
|
|
477
|
+
print("Updated: \(updates.count) objects")
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject> {
|
|
481
|
+
print("Deleted: \(deletes.count) objects")
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Testing Saves
|
|
487
|
+
|
|
488
|
+
### In Unit Tests
|
|
489
|
+
|
|
490
|
+
```swift
|
|
491
|
+
func testSaveArticle() throws {
|
|
492
|
+
let context = testContainer.viewContext
|
|
493
|
+
|
|
494
|
+
let article = Article(context: context)
|
|
495
|
+
article.name = "Test Article"
|
|
496
|
+
|
|
497
|
+
XCTAssertTrue(context.hasChanges)
|
|
498
|
+
|
|
499
|
+
try context.save()
|
|
500
|
+
|
|
501
|
+
XCTAssertFalse(context.hasChanges)
|
|
502
|
+
|
|
503
|
+
// Verify saved
|
|
504
|
+
let fetchRequest = Article.fetchRequest()
|
|
505
|
+
let results = try context.fetch(fetchRequest)
|
|
506
|
+
XCTAssertEqual(results.count, 1)
|
|
507
|
+
XCTAssertEqual(results.first?.name, "Test Article")
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Common Pitfalls
|
|
512
|
+
|
|
513
|
+
### ❌ Not Checking for Changes
|
|
514
|
+
|
|
515
|
+
```swift
|
|
516
|
+
// Wastes resources
|
|
517
|
+
try? context.save()
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### ❌ Saving Too Frequently
|
|
521
|
+
|
|
522
|
+
```swift
|
|
523
|
+
// In a loop - very slow
|
|
524
|
+
for item in items {
|
|
525
|
+
item.processed = true
|
|
526
|
+
try? context.save()
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### ❌ Ignoring Errors
|
|
531
|
+
|
|
532
|
+
```swift
|
|
533
|
+
// Silent failures
|
|
534
|
+
try? context.save()
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### ❌ Saving on Wrong Thread
|
|
538
|
+
|
|
539
|
+
```swift
|
|
540
|
+
// Crash! Background context on main thread
|
|
541
|
+
let context = container.newBackgroundContext()
|
|
542
|
+
try? context.save() // Not in perform block!
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### ✅ Correct Approach
|
|
546
|
+
|
|
547
|
+
```swift
|
|
548
|
+
// Check for changes
|
|
549
|
+
guard context.hasPersistentChanges else { return }
|
|
550
|
+
|
|
551
|
+
// Handle errors
|
|
552
|
+
do {
|
|
553
|
+
try context.save()
|
|
554
|
+
} catch {
|
|
555
|
+
print("Save failed: \(error)")
|
|
556
|
+
// Handle appropriately
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Use correct thread
|
|
560
|
+
context.perform {
|
|
561
|
+
try? context.save()
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Summary
|
|
566
|
+
|
|
567
|
+
1. **Use `saveIfNeeded()` with `hasPersistentChanges`** - Most efficient approach
|
|
568
|
+
2. **Save at appropriate times** - App lifecycle events, after user actions, periodically
|
|
569
|
+
3. **Don't save too frequently** - Debounce auto-saves, avoid saving in loops
|
|
570
|
+
4. **Handle errors properly** - Don't ignore save failures
|
|
571
|
+
5. **Use correct context type** - View context for UI, background for heavy work
|
|
572
|
+
6. **Always use `perform` with background contexts** - Thread safety
|
|
573
|
+
7. **Consider batch operations** - For large-scale updates
|
|
574
|
+
8. **Test your saves** - Verify data persists correctly
|