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:
563
.claude/skills/axiom-storage/SKILL.md
Normal file
563
.claude/skills/axiom-storage/SKILL.md
Normal file
@@ -0,0 +1,563 @@
|
||||
---
|
||||
name: axiom-storage
|
||||
description: Use when asking 'where should I store this data', 'should I use SwiftData or files', 'CloudKit vs iCloud Drive', 'Documents vs Caches', 'local or cloud storage', 'how do I sync data', 'where do app files go' - comprehensive decision framework for all iOS storage options
|
||||
license: MIT
|
||||
compatibility: iOS 17+, iPadOS 17+, macOS Sonoma+
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
last-updated: "2025-12-12"
|
||||
---
|
||||
|
||||
# iOS Storage Guide
|
||||
|
||||
**Purpose**: Navigation hub for ALL storage decisions — database vs files, local vs cloud, specific locations
|
||||
**iOS Version**: iOS 17+ (iOS 26+ for latest features)
|
||||
**Context**: Complete storage decision framework integrating SwiftData (WWDC 2023), CKSyncEngine (WWDC 2023), and file management best practices
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
✅ **Use this skill when**:
|
||||
- Starting a new project and choosing storage approach
|
||||
- Asking "where should I store this data?"
|
||||
- Deciding between SwiftData, Core Data, SQLite, or files
|
||||
- Choosing between CloudKit and iCloud Drive for sync
|
||||
- Determining Documents vs Caches vs Application Support
|
||||
- Planning data architecture for offline/online scenarios
|
||||
- Migrating from one storage solution to another
|
||||
- Debugging "files disappeared" or "data not syncing"
|
||||
|
||||
❌ **Do NOT use this skill for**:
|
||||
- SwiftData implementation details (use `axiom-swiftdata` skill)
|
||||
- SQLite/GRDB specifics (use `axiom-sqlitedata` or `axiom-grdb` skills)
|
||||
- CloudKit sync implementation (use `axiom-cloudkit-ref` skill)
|
||||
- File protection APIs (use `axiom-file-protection-ref` skill)
|
||||
|
||||
**Related Skills**:
|
||||
- Existing database skills: `axiom-swiftdata`, `axiom-sqlitedata`, `axiom-grdb`
|
||||
- New file skills: `axiom-file-protection-ref`, `axiom-storage-management-ref`, `axiom-storage-diag`
|
||||
- New cloud skills: `axiom-cloudkit-ref`, `axiom-icloud-drive-ref`, `axiom-cloud-sync-diag`
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
> **"Choose the right tool for your data shape. Then choose the right location."**
|
||||
|
||||
Storage decisions have two dimensions:
|
||||
1. **Format**: How is data structured? (Queryable records vs files)
|
||||
2. **Location**: Where is it stored? (Local vs cloud, which directory)
|
||||
|
||||
Getting the format wrong forces workarounds. Getting the location wrong causes data loss or backup bloat.
|
||||
|
||||
---
|
||||
|
||||
## The Complete Decision Tree
|
||||
|
||||
### Level 1: Format — What Are You Storing?
|
||||
|
||||
```
|
||||
What is the shape of your data?
|
||||
|
||||
├─ STRUCTURED DATA (queryable records, relationships, search)
|
||||
│ Examples: User profiles, task lists, notes, contacts, transactions
|
||||
│ → Continue to "Structured Data Path" below
|
||||
│
|
||||
└─ FILES (documents, images, videos, downloads, caches)
|
||||
Examples: Photos, PDFs, downloaded content, thumbnails, temp files
|
||||
→ Continue to "File Storage Path" below
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structured Data Path
|
||||
|
||||
### Modern Apps (iOS 17+)
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: SwiftData for modern structured persistence
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
class Task {
|
||||
var title: String
|
||||
var isCompleted: Bool
|
||||
var dueDate: Date
|
||||
|
||||
init(title: String, isCompleted: Bool = false, dueDate: Date) {
|
||||
self.title = title
|
||||
self.isCompleted = isCompleted
|
||||
self.dueDate = dueDate
|
||||
}
|
||||
}
|
||||
|
||||
// Query with type safety
|
||||
@Query(sort: \Task.dueDate) var tasks: [Task]
|
||||
```
|
||||
|
||||
**Why SwiftData**:
|
||||
- Modern Swift-native API (no Objective-C)
|
||||
- Type-safe queries
|
||||
- Built-in CloudKit sync support
|
||||
- Observable models integrate with SwiftUI
|
||||
- **Use skill**: `axiom-swiftdata` for implementation details
|
||||
|
||||
**When NOT to use SwiftData**:
|
||||
- Need advanced SQLite features (FTS5, complex joins)
|
||||
- Existing Core Data app (migration overhead)
|
||||
- Ultra-performance-critical (direct SQLite is faster)
|
||||
|
||||
### Advanced Control Needed
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: SQLiteData or GRDB for advanced features
|
||||
import SQLiteData
|
||||
|
||||
// Full-text search, custom indices, raw SQL when needed
|
||||
let results = try db.prepare("SELECT * FROM users WHERE name MATCH ?", "John")
|
||||
```
|
||||
|
||||
**Use SQLiteData when**:
|
||||
- Need full-text search (FTS5)
|
||||
- Custom SQL queries and indices
|
||||
- Maximum performance (direct SQLite)
|
||||
- Migration from existing SQLite database
|
||||
- **Use skill**: `axiom-sqlitedata` for modern SQLite patterns
|
||||
|
||||
**Use GRDB when**:
|
||||
- Need reactive queries (ValueObservation)
|
||||
- Complex database operations
|
||||
- Type-safe query builders
|
||||
- **Use skill**: `axiom-grdb` for advanced patterns
|
||||
|
||||
### Legacy Apps (iOS 16 and earlier)
|
||||
|
||||
```swift
|
||||
// ❌ LEGACY: Core Data (avoid for new projects)
|
||||
import CoreData
|
||||
|
||||
// NSManagedObject, NSFetchRequest, NSPredicate...
|
||||
```
|
||||
|
||||
**Only use Core Data if**:
|
||||
- Maintaining existing Core Data app
|
||||
- Can't upgrade to iOS 17 minimum deployment
|
||||
|
||||
---
|
||||
|
||||
## File Storage Path
|
||||
|
||||
### Decision Tree for Files
|
||||
|
||||
```
|
||||
What kind of file is it?
|
||||
|
||||
├─ USER-CREATED CONTENT (documents, photos created by user)
|
||||
│ Where: Documents/ directory
|
||||
│ Backed up: ✅ Yes (iCloud/iTunes)
|
||||
│ Purged: ❌ Never
|
||||
│ Visible in Files app: ✅ Yes
|
||||
│ Example: User's edited photos, documents, exported data
|
||||
│ → See "Documents Directory" section below
|
||||
│
|
||||
├─ APP-GENERATED DATA (not user-visible, must persist)
|
||||
│ Where: Library/Application Support/
|
||||
│ Backed up: ✅ Yes
|
||||
│ Purged: ❌ Never
|
||||
│ Visible in Files app: ❌ No
|
||||
│ Example: Database files, user settings, downloaded assets
|
||||
│ → See "Application Support Directory" section below
|
||||
│
|
||||
├─ RE-DOWNLOADABLE / REGENERABLE CONTENT
|
||||
│ Where: Library/Caches/
|
||||
│ Backed up: ❌ No (set isExcludedFromBackup)
|
||||
│ Purged: ✅ Yes (under storage pressure)
|
||||
│ Example: Thumbnails, API responses, downloaded images
|
||||
│ → See "Caches Directory" section below
|
||||
│
|
||||
└─ TEMPORARY FILES (can be deleted anytime)
|
||||
Where: tmp/
|
||||
Backed up: ❌ No
|
||||
Purged: ✅ Yes (aggressive, even while app running)
|
||||
Example: Image processing intermediates, export staging
|
||||
→ See "Temporary Directory" section below
|
||||
```
|
||||
|
||||
### Documents Directory
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: User-created content in Documents
|
||||
func saveUserDocument(_ data: Data, filename: String) throws {
|
||||
let documentsURL = FileManager.default.urls(
|
||||
for: .documentDirectory,
|
||||
in: .userDomainMask
|
||||
)[0]
|
||||
|
||||
let fileURL = documentsURL.appendingPathComponent(filename)
|
||||
|
||||
// Enable file protection
|
||||
try data.write(to: fileURL, options: .completeFileProtection)
|
||||
}
|
||||
```
|
||||
|
||||
**Key rules**:
|
||||
- ✅ DO store: User-created documents, exported files, user-visible content
|
||||
- ❌ DON'T store: Downloaded data that can be re-fetched, caches, temp files
|
||||
- ⚠️ WARNING: Everything here is backed up to iCloud. Large re-downloadable files will bloat backups and may get your app rejected.
|
||||
|
||||
**Use skill**: `axiom-file-protection-ref` for encryption options
|
||||
|
||||
### Application Support Directory
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: App data in Application Support
|
||||
func getAppDataURL() -> URL {
|
||||
let appSupportURL = FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
)[0]
|
||||
|
||||
// Create app-specific subdirectory
|
||||
let appDataURL = appSupportURL.appendingPathComponent(
|
||||
Bundle.main.bundleIdentifier ?? "AppData"
|
||||
)
|
||||
|
||||
try? FileManager.default.createDirectory(
|
||||
at: appDataURL,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
|
||||
return appDataURL
|
||||
}
|
||||
```
|
||||
|
||||
**Use for**:
|
||||
- SwiftData/SQLite database files
|
||||
- User preferences
|
||||
- Downloaded assets that must persist
|
||||
- Configuration files
|
||||
|
||||
### Caches Directory
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Re-downloadable content in Caches
|
||||
func cacheDownloadedImage(data: Data, for url: URL) throws {
|
||||
let cacheURL = FileManager.default.urls(
|
||||
for: .cachesDirectory,
|
||||
in: .userDomainMask
|
||||
)[0]
|
||||
|
||||
let filename = url.lastPathComponent
|
||||
let fileURL = cacheURL.appendingPathComponent(filename)
|
||||
|
||||
try data.write(to: fileURL)
|
||||
|
||||
// Mark as excluded from backup (explicit, though Caches is auto-excluded)
|
||||
var resourceValues = URLResourceValues()
|
||||
resourceValues.isExcludedFromBackup = true
|
||||
try fileURL.setResourceValues(resourceValues)
|
||||
}
|
||||
```
|
||||
|
||||
**Key rules**:
|
||||
- ✅ The system CAN and WILL delete files here under storage pressure
|
||||
- ✅ Always have a way to re-download or regenerate
|
||||
- ❌ Don't store anything that can't be recreated
|
||||
|
||||
**Use skill**: `axiom-storage-management-ref` for purge policies and disk space management
|
||||
|
||||
### Temporary Directory
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Truly temporary files in tmp
|
||||
func processImageWithTempFile(image: UIImage) throws {
|
||||
let tmpURL = FileManager.default.temporaryDirectory
|
||||
let tempFileURL = tmpURL.appendingPathComponent(UUID().uuidString + ".jpg")
|
||||
|
||||
// Write temp file
|
||||
try image.jpegData(compressionQuality: 0.8)?.write(to: tempFileURL)
|
||||
|
||||
// Process...
|
||||
processImage(at: tempFileURL)
|
||||
|
||||
// Clean up (though system will auto-clean eventually)
|
||||
try? FileManager.default.removeItem(at: tempFileURL)
|
||||
}
|
||||
```
|
||||
|
||||
**Key rules**:
|
||||
- System can delete files here AT ANY TIME (even while app is running)
|
||||
- Always clean up after yourself
|
||||
- Don't rely on files persisting between app launches
|
||||
|
||||
---
|
||||
|
||||
## Cloud Storage Decisions
|
||||
|
||||
### Should Data Sync to Cloud?
|
||||
|
||||
```
|
||||
Does this data need to sync across user's devices?
|
||||
|
||||
├─ NO → Use local storage (paths above)
|
||||
│
|
||||
└─ YES → What kind of data?
|
||||
│
|
||||
├─ STRUCTURED DATA (queryable, relationships)
|
||||
│ → Use CloudKit
|
||||
│ → See "CloudKit Path" below
|
||||
│
|
||||
├─ FILES (documents, images)
|
||||
│ → Use iCloud Drive (ubiquitous containers)
|
||||
│ → See "iCloud Drive Path" below
|
||||
│
|
||||
└─ SMALL PREFERENCES (<1 MB, key-value pairs)
|
||||
→ Use NSUbiquitousKeyValueStore
|
||||
→ See "Key-Value Store" below
|
||||
```
|
||||
|
||||
### CloudKit Path (Structured Data Sync)
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: SwiftData with CloudKit sync (iOS 17+)
|
||||
import SwiftData
|
||||
|
||||
let container = try ModelContainer(
|
||||
for: Task.self,
|
||||
configurations: ModelConfiguration(
|
||||
cloudKitDatabase: .private("iCloud.com.example.app")
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
**Three approaches to CloudKit**:
|
||||
|
||||
1. **SwiftData + CloudKit** (Recommended, iOS 17+):
|
||||
- Automatic sync for SwiftData models
|
||||
- Private database only
|
||||
- Easiest approach
|
||||
- **Use skill**: `axiom-swiftdata` for details
|
||||
|
||||
2. **CKSyncEngine** (Custom persistence, iOS 17+):
|
||||
- For SQLite, GRDB, or custom stores
|
||||
- Manages sync automatically
|
||||
- Modern replacement for manual CloudKit
|
||||
- **Use skill**: `axiom-cloudkit-ref` for CKSyncEngine patterns
|
||||
|
||||
3. **Raw CloudKit APIs** (Legacy):
|
||||
- CKContainer, CKDatabase, CKRecord
|
||||
- Manual sync management
|
||||
- Only if CKSyncEngine doesn't fit
|
||||
- **Use skill**: `axiom-cloudkit-ref` for raw API reference
|
||||
|
||||
### iCloud Drive Path (File Sync)
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: iCloud Drive for file-based sync
|
||||
func saveToICloud(_ data: Data, filename: String) throws {
|
||||
// Get ubiquitous container
|
||||
guard let iCloudURL = FileManager.default.url(
|
||||
forUbiquityContainerIdentifier: nil
|
||||
) else {
|
||||
throw StorageError.iCloudUnavailable
|
||||
}
|
||||
|
||||
let documentsURL = iCloudURL.appendingPathComponent("Documents")
|
||||
try FileManager.default.createDirectory(
|
||||
at: documentsURL,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
|
||||
let fileURL = documentsURL.appendingPathComponent(filename)
|
||||
try data.write(to: fileURL)
|
||||
}
|
||||
```
|
||||
|
||||
**When to use iCloud Drive**:
|
||||
- User-created documents that sync
|
||||
- File-based collaboration
|
||||
- Simple file sync (like Dropbox)
|
||||
|
||||
**Use skill**: `axiom-icloud-drive-ref` for implementation details
|
||||
|
||||
### Key-Value Store (Small Preferences)
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Small synced preferences
|
||||
let store = NSUbiquitousKeyValueStore.default
|
||||
|
||||
store.set(true, forKey: "darkModeEnabled")
|
||||
store.set(2.0, forKey: "textSize")
|
||||
store.synchronize()
|
||||
```
|
||||
|
||||
**Limitations**:
|
||||
- Max 1 MB total storage
|
||||
- Max 1024 keys
|
||||
- Max 1 MB per value
|
||||
- For preferences ONLY, not data storage
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns and Anti-Patterns
|
||||
|
||||
### ✅ DO: Choose Based on Data Shape
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Structured data → SwiftData
|
||||
@Model
|
||||
class Note {
|
||||
var title: String
|
||||
var content: String
|
||||
var tags: [Tag] // Relationships
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Files → FileManager + proper directory
|
||||
let imageData = capturedPhoto.jpegData(compressionQuality: 0.9)
|
||||
try imageData?.write(to: documentsURL.appendingPathComponent("photo.jpg"))
|
||||
```
|
||||
|
||||
### ❌ DON'T: Use Files for Structured Data
|
||||
|
||||
```swift
|
||||
// ❌ WRONG: Storing queryable data as JSON files
|
||||
let tasks = [Task(...), Task(...), Task(...)]
|
||||
let jsonData = try JSONEncoder().encode(tasks)
|
||||
try jsonData.write(to: appSupportURL.appendingPathComponent("tasks.json"))
|
||||
|
||||
// Why it's wrong:
|
||||
// - Can't query individual tasks
|
||||
// - Can't filter or sort efficiently
|
||||
// - No relationships
|
||||
// - Entire file loaded into memory
|
||||
// - Concurrent access issues
|
||||
|
||||
// ✅ CORRECT: Use SwiftData instead
|
||||
@Model class Task { ... }
|
||||
```
|
||||
|
||||
### ❌ DON'T: Store Re-downloadable Content in Documents
|
||||
|
||||
```swift
|
||||
// ❌ WRONG: Downloaded images in Documents (bloats backup!)
|
||||
func downloadProfileImage(url: URL) throws {
|
||||
let data = try Data(contentsOf: url)
|
||||
let documentsURL = FileManager.default.urls(
|
||||
for: .documentDirectory,
|
||||
in: .userDomainMask
|
||||
)[0]
|
||||
try data.write(to: documentsURL.appendingPathComponent("profile.jpg"))
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Use Caches instead
|
||||
func downloadProfileImage(url: URL) throws {
|
||||
let data = try Data(contentsOf: url)
|
||||
let cacheURL = FileManager.default.urls(
|
||||
for: .cachesDirectory,
|
||||
in: .userDomainMask
|
||||
)[0]
|
||||
let fileURL = cacheURL.appendingPathComponent("profile.jpg")
|
||||
try data.write(to: fileURL)
|
||||
|
||||
// Mark excluded from backup
|
||||
var resourceValues = URLResourceValues()
|
||||
resourceValues.isExcludedFromBackup = true
|
||||
try fileURL.setResourceValues(resourceValues)
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ DON'T: Use CloudKit for Simple File Sync
|
||||
|
||||
```swift
|
||||
// ❌ WRONG: Storing files as CKAssets with manual sync
|
||||
let asset = CKAsset(fileURL: documentURL)
|
||||
let record = CKRecord(recordType: "Document")
|
||||
record["file"] = asset
|
||||
// ... manual upload, conflict handling, etc.
|
||||
|
||||
// ✅ CORRECT: Use iCloud Drive for files
|
||||
// Files automatically sync via ubiquitous container
|
||||
try data.write(to: iCloudDocumentsURL.appendingPathComponent("doc.pdf"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Table
|
||||
|
||||
| Data Type | Format | Local Location | Cloud Sync | Use Skill |
|
||||
|-----------|--------|----------------|------------|-----------|
|
||||
| User tasks, notes | Structured | Application Support | SwiftData + CloudKit | `axiom-swiftdata` → `axiom-cloudkit-ref` |
|
||||
| User photos (created) | File | Documents | iCloud Drive | `axiom-file-protection-ref` → `axiom-icloud-drive-ref` |
|
||||
| Downloaded images | File | Caches | None (re-download) | `axiom-storage-management-ref` |
|
||||
| Thumbnails | File | Caches | None (regenerate) | `axiom-storage-management-ref` |
|
||||
| Database file | File | Application Support | CKSyncEngine (if custom) | `axiom-sqlitedata` → `axiom-cloudkit-ref` |
|
||||
| Temp processing | File | tmp | None | N/A |
|
||||
| User settings | Key-Value | UserDefaults | NSUbiquitousKeyValueStore | N/A |
|
||||
|
||||
---
|
||||
|
||||
## tvOS Storage
|
||||
|
||||
**tvOS has no persistent local storage.** This catches every iOS developer.
|
||||
|
||||
| Directory | tvOS Behavior |
|
||||
|-----------|--------------|
|
||||
| Documents | Does not exist |
|
||||
| Application Support | System can delete when app is not running |
|
||||
| Caches | System deletes at any time |
|
||||
| tmp | System deletes at any time |
|
||||
| UserDefaults | 500 KB limit (vs ~4 MB on iOS) |
|
||||
|
||||
**Every local file can vanish between app launches.** Your tvOS app must survive starting from zero.
|
||||
|
||||
**Recommended**: Use iCloud (CloudKit, NSUbiquitousKeyValueStore, or iCloud Drive) as primary storage. Treat local files as cache only. See `axiom-tvos` for full tvOS storage patterns.
|
||||
|
||||
---
|
||||
|
||||
## Debugging: Data Missing or Not Syncing?
|
||||
|
||||
**Files disappeared**:
|
||||
- Check if stored in Caches or tmp (system purged them)
|
||||
- Check file protection level (may be inaccessible when locked)
|
||||
- **Use skill**: `axiom-storage-diag`
|
||||
|
||||
**Backup too large**:
|
||||
- Check if re-downloadable content is in Documents (should be in Caches)
|
||||
- Check if `isExcludedFromBackup` is set on large files
|
||||
- **Use skill**: `axiom-storage-management-ref`
|
||||
|
||||
**Data not syncing**:
|
||||
- CloudKit: Check CKSyncEngine status, account availability
|
||||
- **Use skill**: `axiom-cloud-sync-diag`
|
||||
- iCloud Drive: Check ubiquitous container entitlements, file coordinator
|
||||
- **Use skill**: `axiom-icloud-drive-ref`, `axiom-cloud-sync-diag`
|
||||
|
||||
---
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
When changing storage approach:
|
||||
|
||||
**Database to Database** (e.g., Core Data → SwiftData):
|
||||
- [ ] Create SwiftData models matching Core Data entities
|
||||
- [ ] Write migration code to copy data
|
||||
- [ ] Test with production-size datasets
|
||||
- [ ] Keep old database for rollback
|
||||
|
||||
**Files to Database**:
|
||||
- [ ] Identify all JSON/plist files storing structured data
|
||||
- [ ] Create SwiftData models
|
||||
- [ ] Write one-time migration on first launch
|
||||
- [ ] Verify all data migrated, then delete old files
|
||||
|
||||
**Local to Cloud**:
|
||||
- [ ] Ensure proper entitlements (CloudKit/iCloud)
|
||||
- [ ] Handle initial upload carefully (bandwidth)
|
||||
- [ ] Test conflict resolution
|
||||
- [ ] Provide user control (opt-in)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-12
|
||||
**Skill Type**: Discipline
|
||||
**Related WWDC Sessions**:
|
||||
- WWDC 2023-10187: Meet SwiftData
|
||||
- WWDC 2023-10188: Sync to iCloud with CKSyncEngine
|
||||
- WWDC 2024-10137: What's new in SwiftData
|
||||
Reference in New Issue
Block a user