Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-core-data/.openskills.json
Normal file
7
.claude/skills/axiom-core-data/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-core-data",
|
||||
"installedAt": "2026-04-12T08:06:06.545Z"
|
||||
}
|
||||
417
.claude/skills/axiom-core-data/SKILL.md
Normal file
417
.claude/skills/axiom-core-data/SKILL.md
Normal file
@@ -0,0 +1,417 @@
|
||||
---
|
||||
name: axiom-core-data
|
||||
description: Use when choosing Core Data vs SwiftData, setting up the Core Data stack, modeling relationships, or implementing concurrency patterns - prevents thread-confinement errors and migration crashes
|
||||
license: MIT
|
||||
compatibility: iOS 3+, macOS 10.4+
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
last-updated: "2025-12-25"
|
||||
---
|
||||
|
||||
# Core Data
|
||||
|
||||
## Overview
|
||||
|
||||
**Core principle**: Core Data is a mature object graph and persistence framework. Use it when needing features SwiftData doesn't support, or when targeting older iOS versions.
|
||||
|
||||
**When to use Core Data vs SwiftData**:
|
||||
- **SwiftData** (iOS 17+) — New apps, simpler API, Swift-native
|
||||
- **Core Data** — iOS 16 and earlier, advanced features, existing codebases
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
```
|
||||
Which persistence framework?
|
||||
|
||||
├─ Targeting iOS 17+ only?
|
||||
│ ├─ Simple data model? → SwiftData (recommended)
|
||||
│ ├─ Need public CloudKit database? → Core Data (SwiftData is private-only)
|
||||
│ ├─ Need custom migration logic? → Core Data (more control)
|
||||
│ └─ Existing Core Data app? → Keep Core Data or migrate gradually
|
||||
│
|
||||
├─ Targeting iOS 16 or earlier?
|
||||
│ └─ Core Data (SwiftData unavailable)
|
||||
│
|
||||
└─ Need both? → Use Core Data with SwiftData wrapper (advanced)
|
||||
```
|
||||
|
||||
## Red Flags
|
||||
|
||||
If ANY of these appear, STOP:
|
||||
|
||||
- ❌ "Access managed objects on any thread" — Thread-confinement violation
|
||||
- ❌ "Skip migration testing on real device" — Simulator hides schema issues
|
||||
- ❌ "Use a singleton context everywhere" — Leads to concurrency crashes
|
||||
- ❌ "Force lightweight migration always" — Complex changes need mapping models
|
||||
- ❌ "Fetch in view body" — Use @FetchRequest or observe in view model
|
||||
|
||||
## Core Data Stack Setup
|
||||
|
||||
### Modern Stack (iOS 10+)
|
||||
|
||||
```swift
|
||||
import CoreData
|
||||
|
||||
class CoreDataStack {
|
||||
static let shared = CoreDataStack()
|
||||
|
||||
lazy var persistentContainer: NSPersistentContainer = {
|
||||
let container = NSPersistentContainer(name: "Model")
|
||||
|
||||
// Configure for CloudKit if needed
|
||||
// container.persistentStoreDescriptions.first?.cloudKitContainerOptions =
|
||||
// NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.app")
|
||||
|
||||
container.loadPersistentStores { description, error in
|
||||
if let error = error {
|
||||
// Handle appropriately for production
|
||||
fatalError("Failed to load store: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Enable automatic merging
|
||||
container.viewContext.automaticallyMergesChangesFromParent = true
|
||||
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
|
||||
return container
|
||||
}()
|
||||
|
||||
var viewContext: NSManagedObjectContext {
|
||||
persistentContainer.viewContext
|
||||
}
|
||||
|
||||
func newBackgroundContext() -> NSManagedObjectContext {
|
||||
persistentContainer.newBackgroundContext()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CloudKit Integration
|
||||
|
||||
```swift
|
||||
import CoreData
|
||||
|
||||
class CloudKitStack {
|
||||
lazy var container: NSPersistentCloudKitContainer = {
|
||||
let container = NSPersistentCloudKitContainer(name: "Model")
|
||||
|
||||
guard let description = container.persistentStoreDescriptions.first else {
|
||||
fatalError("No store description")
|
||||
}
|
||||
|
||||
// Enable CloudKit sync
|
||||
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
|
||||
containerIdentifier: "iCloud.com.yourapp"
|
||||
)
|
||||
|
||||
// Enable history tracking for sync
|
||||
description.setOption(true as NSNumber,
|
||||
forKey: NSPersistentHistoryTrackingKey)
|
||||
description.setOption(true as NSNumber,
|
||||
forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
|
||||
|
||||
container.loadPersistentStores { _, error in
|
||||
if let error = error {
|
||||
fatalError("CloudKit store failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
container.viewContext.automaticallyMergesChangesFromParent = true
|
||||
|
||||
return container
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
## Concurrency Patterns
|
||||
|
||||
### The Golden Rule
|
||||
|
||||
**NEVER pass NSManagedObject across threads.** Pass objectID instead.
|
||||
|
||||
```swift
|
||||
// ❌ WRONG: Passing object across threads
|
||||
let user = viewContext.fetch(...) // Main thread
|
||||
Task.detached {
|
||||
print(user.name) // CRASH: Wrong thread
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Pass objectID, fetch on target context
|
||||
let userID = user.objectID
|
||||
Task.detached {
|
||||
let bgContext = CoreDataStack.shared.newBackgroundContext()
|
||||
let user = bgContext.object(with: userID) as! User
|
||||
print(user.name) // Safe
|
||||
}
|
||||
```
|
||||
|
||||
### Background Processing
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Background context for heavy work
|
||||
func importData(_ items: [ImportItem]) async throws {
|
||||
let context = CoreDataStack.shared.newBackgroundContext()
|
||||
|
||||
try await context.perform {
|
||||
for item in items {
|
||||
let entity = Entity(context: context)
|
||||
entity.configure(from: item)
|
||||
}
|
||||
|
||||
try context.save()
|
||||
}
|
||||
}
|
||||
|
||||
// Changes automatically merge to viewContext if configured
|
||||
```
|
||||
|
||||
### Async/Await (iOS 15+)
|
||||
|
||||
```swift
|
||||
// Modern async context operations
|
||||
func fetchUsers() async throws -> [User] {
|
||||
let context = CoreDataStack.shared.viewContext
|
||||
|
||||
return try await context.perform {
|
||||
let request = User.fetchRequest()
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
|
||||
return try context.fetch(request)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Relationship Modeling
|
||||
|
||||
### One-to-Many
|
||||
|
||||
```swift
|
||||
// In User entity
|
||||
@NSManaged var posts: NSSet?
|
||||
|
||||
// Convenience accessors
|
||||
extension User {
|
||||
var postsArray: [Post] {
|
||||
(posts?.allObjects as? [Post]) ?? []
|
||||
}
|
||||
|
||||
func addPost(_ post: Post) {
|
||||
mutableSetValue(forKey: "posts").add(post)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Many-to-Many
|
||||
|
||||
```swift
|
||||
// Both sides have NSSet
|
||||
// User.tags <-> Tag.users
|
||||
|
||||
extension User {
|
||||
func addTag(_ tag: Tag) {
|
||||
mutableSetValue(forKey: "tags").add(tag)
|
||||
// Core Data automatically adds to tag.users
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Rules
|
||||
|
||||
| Rule | Behavior | Use Case |
|
||||
|------|----------|----------|
|
||||
| **Nullify** | Set relationship to nil | Optional relationships |
|
||||
| **Cascade** | Delete related objects | Owned children (User → Posts) |
|
||||
| **Deny** | Prevent deletion if related objects exist | Protect referenced data |
|
||||
| **No Action** | Do nothing (manual cleanup required) | Rarely appropriate |
|
||||
|
||||
## Fetching Patterns
|
||||
|
||||
### SwiftUI Integration
|
||||
|
||||
```swift
|
||||
struct UserList: View {
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \User.name, ascending: true)],
|
||||
predicate: NSPredicate(format: "isActive == YES"),
|
||||
animation: .default
|
||||
)
|
||||
private var users: FetchedResults<User>
|
||||
|
||||
var body: some View {
|
||||
List(users) { user in
|
||||
Text(user.name ?? "Unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic predicates
|
||||
struct FilteredList: View {
|
||||
@FetchRequest var items: FetchedResults<Item>
|
||||
|
||||
init(category: String) {
|
||||
_items = FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \Item.date, ascending: false)],
|
||||
predicate: NSPredicate(format: "category == %@", category)
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Fetching (Avoid N+1)
|
||||
|
||||
```swift
|
||||
// ❌ WRONG: N+1 queries
|
||||
let users = try context.fetch(User.fetchRequest())
|
||||
for user in users {
|
||||
print(user.posts?.count ?? 0) // Fault fired for each user
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Prefetch relationships
|
||||
let request = User.fetchRequest()
|
||||
request.relationshipKeyPathsForPrefetching = ["posts"]
|
||||
let users = try context.fetch(request)
|
||||
for user in users {
|
||||
print(user.posts?.count ?? 0) // Already loaded
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Size for Large Datasets
|
||||
|
||||
```swift
|
||||
let request = User.fetchRequest()
|
||||
request.fetchBatchSize = 20 // Load 20 at a time as needed
|
||||
request.returnsObjectsAsFaults = true // Default, memory efficient
|
||||
```
|
||||
|
||||
## Schema Migration
|
||||
|
||||
### Lightweight Migration (Automatic)
|
||||
|
||||
Handled automatically for:
|
||||
- Adding optional attributes
|
||||
- Removing attributes
|
||||
- Renaming (with renaming identifier)
|
||||
- Adding relationships with optional or default value
|
||||
|
||||
```swift
|
||||
let description = NSPersistentStoreDescription()
|
||||
description.shouldMigrateStoreAutomatically = true
|
||||
description.shouldInferMappingModelAutomatically = true
|
||||
```
|
||||
|
||||
### When Mapping Model Is Needed
|
||||
|
||||
- Changing attribute types
|
||||
- Splitting/merging entities
|
||||
- Complex relationship changes
|
||||
- Data transformation during migration
|
||||
|
||||
```swift
|
||||
// Create mapping model in Xcode:
|
||||
// File → New → Mapping Model
|
||||
// Select source and destination models
|
||||
```
|
||||
|
||||
### Migration Testing Checklist
|
||||
|
||||
**MANDATORY before shipping**:
|
||||
|
||||
1. ✓ Test on REAL DEVICE (simulator deletes DB on rebuild)
|
||||
2. ✓ Install old version, create data
|
||||
3. ✓ Install new version over it
|
||||
4. ✓ Verify all data accessible
|
||||
5. ✓ Check migration performance (large datasets)
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### 1. Singleton Context for Everything
|
||||
|
||||
```swift
|
||||
// ❌ WRONG: One context for all operations
|
||||
class DataManager {
|
||||
let context = CoreDataStack.shared.viewContext
|
||||
|
||||
func importInBackground() {
|
||||
// Using main context on background = crash
|
||||
for item in largeDataset {
|
||||
let entity = Entity(context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Context per operation type
|
||||
func importInBackground() {
|
||||
let bgContext = CoreDataStack.shared.newBackgroundContext()
|
||||
bgContext.perform {
|
||||
// Safe background work
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Fetching in View Body
|
||||
|
||||
```swift
|
||||
// ❌ WRONG: Fetch on every render
|
||||
var body: some View {
|
||||
let users = try? context.fetch(User.fetchRequest()) // Called repeatedly!
|
||||
List(users ?? []) { ... }
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Use @FetchRequest
|
||||
@FetchRequest(sortDescriptors: [])
|
||||
var users: FetchedResults<User>
|
||||
|
||||
var body: some View {
|
||||
List(users) { ... } // Automatic updates
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Ignoring Merge Policy
|
||||
|
||||
```swift
|
||||
// ❌ WRONG: No merge policy (conflicts crash)
|
||||
let context = container.viewContext
|
||||
|
||||
// ✅ CORRECT: Define merge behavior
|
||||
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
context.automaticallyMergesChangesFromParent = true
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use fetchBatchSize** for large result sets
|
||||
2. **Prefetch relationships** that will be accessed
|
||||
3. **Use background contexts** for imports/exports
|
||||
4. **Batch save** — don't save after each insert
|
||||
5. **Use fetchLimit** when only first N results are needed
|
||||
6. **Profile with SQL debug**: `-com.apple.CoreData.SQLDebug 1`
|
||||
|
||||
## Pressure Scenarios
|
||||
|
||||
### Scenario 1: "SwiftData is simpler, let's migrate now"
|
||||
|
||||
**Situation**: New iOS 17 features available, temptation to migrate mid-project.
|
||||
|
||||
**Risk**: Migration is complex. Mixed Core Data + SwiftData has sharp edges.
|
||||
|
||||
**Response**: "Complete current milestone first. Migration needs dedicated time and testing."
|
||||
|
||||
### Scenario 2: "Skip migration testing, simulator works"
|
||||
|
||||
**Situation**: Schema change tested only in simulator.
|
||||
|
||||
**Risk**: Simulator deletes database on rebuild. Real devices keep persistent data and crash.
|
||||
|
||||
**Response**: "MANDATORY: Test on real device with real data. 15 minutes now prevents production crash."
|
||||
|
||||
## tvOS
|
||||
|
||||
**CoreData + CloudKit is dangerous on tvOS.** CloudKit metadata causes significant space inflation in the local store, and tvOS has no persistent local storage — the system deletes Caches (including Application Support) at any time. The inflated store plus random deletion is a worst-case combination.
|
||||
|
||||
**Recommendation**: Use SQLiteData with CloudKit SyncEngine instead for tvOS data persistence. See `axiom-tvos` for full tvOS storage constraints.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `axiom-core-data-diag` — Debugging migrations, thread errors, N+1 queries
|
||||
- `axiom-swiftdata` — Modern alternative for iOS 17+
|
||||
- `axiom-database-migration` — Safe schema evolution patterns
|
||||
- `axiom-swift-concurrency` — Async/await patterns for Core Data
|
||||
3
.claude/skills/axiom-core-data/agents/openai.yaml
Normal file
3
.claude/skills/axiom-core-data/agents/openai.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "Core Data"
|
||||
short_description: "Choosing Core Data vs SwiftData, setting up the Core Data stack, modeling relationships, or implementing concurrency ..."
|
||||
Reference in New Issue
Block a user