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.
564 lines
17 KiB
Markdown
564 lines
17 KiB
Markdown
---
|
|
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
|