--- 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