| name | storage-strategy |
| 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 |
| skill_type | discipline |
| version | 1.0.0 |
| last_updated | Fri Dec 12 2025 00:00:00 GMT+0000 (Coordinated Universal Time) |
| apple_platforms | iOS 17+, iPadOS 17+, macOS Sonoma+ |
iOS Storage Strategy 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
swiftdataskill) - SQLite/GRDB specifics (use
sqlitedataorgrdbskills) - CloudKit sync implementation (use
cloudkit-refskill) - File protection APIs (use
file-protection-refskill)
Related Skills:
- Existing database skills:
swiftdata,sqlitedata,grdb - New file skills:
file-protection-ref,storage-management-ref,storage-diag - New cloud skills:
cloudkit-ref,icloud-drive-ref,cloud-sync-diag
Core Philosophy
"Choose the right tool for your data shape. Then choose the right location."
Storage decisions have two dimensions:
- Format: How is data structured? (Queryable records vs files)
- 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+)
// ✅ 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:
swiftdatafor 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
// ✅ 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:
sqlitedatafor modern SQLite patterns
Use GRDB when:
- Need reactive queries (ValueObservation)
- Complex database operations
- Type-safe query builders
- Use skill:
grdbfor advanced patterns
Legacy Apps (iOS 16 and earlier)
// ❌ 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
// ✅ 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: file-protection-ref for encryption options
Application Support Directory
// ✅ 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
// ✅ 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: storage-management-ref for purge policies and disk space management
Temporary Directory
// ✅ 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)
// ✅ 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:
SwiftData + CloudKit (Recommended, iOS 17+):
- Automatic sync for SwiftData models
- Private database only
- Easiest approach
- Use skill:
swiftdatafor details
CKSyncEngine (Custom persistence, iOS 17+):
- For SQLite, GRDB, or custom stores
- Manages sync automatically
- Modern replacement for manual CloudKit
- Use skill:
cloudkit-reffor CKSyncEngine patterns
Raw CloudKit APIs (Legacy):
- CKContainer, CKDatabase, CKRecord
- Manual sync management
- Only if CKSyncEngine doesn't fit
- Use skill:
cloudkit-reffor raw API reference
iCloud Drive Path (File Sync)
// ✅ 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: icloud-drive-ref for implementation details
Key-Value Store (Small Preferences)
// ✅ 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
// ✅ 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
// ❌ 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
// ❌ 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
// ❌ 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 | swiftdata → cloudkit-ref |
| User photos (created) | File | Documents | iCloud Drive | file-protection-ref → icloud-drive-ref |
| Downloaded images | File | Caches | None (re-download) | storage-management-ref |
| Thumbnails | File | Caches | None (regenerate) | storage-management-ref |
| Database file | File | Application Support | CKSyncEngine (if custom) | sqlitedata → cloudkit-ref |
| Temp processing | File | tmp | None | N/A |
| User settings | Key-Value | UserDefaults | NSUbiquitousKeyValueStore | N/A |
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:
storage-diag
Backup too large:
- Check if re-downloadable content is in Documents (should be in Caches)
- Check if
isExcludedFromBackupis set on large files - Use skill:
storage-management-ref
Data not syncing:
- CloudKit: Check CKSyncEngine status, account availability
- Use skill:
cloud-sync-diag
- Use skill:
- iCloud Drive: Check ubiquitous container entitlements, file coordinator
- Use skill:
icloud-drive-ref,cloud-sync-diag
- Use skill:
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