Working with Data Persistence
1. Using UserDefaults for Simple Storage
| Property | Type | Description |
|---|---|---|
| standard | UserDefaults |
Shared instance of UserDefaults for the current app |
| suiteName | String? |
Name of the app group to use for storage |
Example: Saving and Retrieving Data
let defaults = UserDefaults.standard
// Saving data
defaults.set("John Doe", forKey: "username")
defaults.set(25, forKey: "age")
defaults.set(true, forKey: "isSubscribed")
// Retrieving data
let username = defaults.string(forKey: "username")
let age = defaults.integer(forKey: "age")
let isSubscribed = defaults.bool(forKey: "isSubscribed")
Example: Working with Arrays and Dictionaries
let defaults = UserDefaults.standard
// Saving arrays and dictionaries
let hobbies = ["reading", "swimming", "coding"]
let userPreferences = [
"theme": "dark",
"notifications": true
]
defaults.set(hobbies, forKey: "hobbies")
defaults.set(userPreferences, forKey: "preferences")
// Retrieving arrays and dictionaries
let savedHobbies = defaults.array(forKey: "hobbies") as? [String]
let savedPreferences = defaults.dictionary(forKey: "preferences") as? [String: Any]
Note: UserDefaults is not suitable for storing large amounts of data or sensitive information. It's designed for simple key-value pairs and small configuration settings.
2. Storing and Retrieving Objects
| Method | Description | Code Example |
|---|---|---|
| Core Data | Framework for managing the model layer of object-oriented applications. | |
| UserDefaults | Simple key-value storage for small amounts of data. | |
| Archiving with NSCoder | Serializes objects to and from data using NSCoder. | |
Note: Core Data is preferred for complex data models, while UserDefaults is suitable for simple settings or preferences.
3. Working with File Manager
| Method | Description | Notes |
|---|---|---|
| FileManager.default.urls(for:in:) | Retrieves URLs for standard directories like Documents, Library, etc. | Use .documentDirectory for app-specific documents |
| FileManager.default.createDirectory(atPath:withIntermediateDirectories:attributes:) | Creates a directory at the specified path. | Set withIntermediateDirectories to true to create parent directories |
| FileManager.default.contentsOfDirectory(atPath:) | Returns an array of file names in the specified directory. | Useful for listing contents of a folder |
| FileManager.default.fileExists(atPath:) | Checks whether a file or directory exists at the specified path. | Can be used to verify existence before reading or writing |
| FileManager.default.removeItem(atPath:) | Deletes a file or directory at the specified path. | Use with caution as this action is irreversible |
Example: Save Data to Documents Directory
import Foundation
let fileManager = FileManager.default
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Could not access documents directory")
return
}
let fileURL = documentsDirectory.appendingPathComponent("data.json")
let data = "Hello, World!".data(using: .utf8)!
do {
try data.write(to: fileURL)
print("Data written to \(fileURL)")
} catch {
print("Failed to write data: \(error)")
}
Example: Read Data from Documents Directory
import Foundation
let fileManager = FileManager.default
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Could not access documents directory")
return
}
let fileURL = documentsDirectory.appendingPathComponent("data.json")
do {
let data = try Data(contentsOf: fileURL)
let string = String(data: data, encoding: .utf8)
print("Read data: \(string ?? "Empty")")
} catch {
print("Failed to read data: \(error)")
}
Note: Always handle errors when working with file operations to prevent crashes or unexpected behavior.
4. Reading Files from Bundle
| File Type | Description | Method | Example |
|---|---|---|---|
| JSON | Structured data file | Bundle.main.path(forResource: "data", ofType: "json") |
Example: Reading JSON from Bundle |
| plist | Property list file | Bundle.main.path(forResource: "config", ofType: "plist") |
Example: Reading plist from Bundle |
| txt | Plain text file | Bundle.main.url(forResource: "readme", withExtension: "txt") |
Example: Reading txt from Bundle |
Note: Files must be added to the app bundle during build time. Runtime file creation or modification is not possible from within the app's sandbox.
5. Writing Files to Documents Directory
| Method | Description | Notes |
|---|---|---|
| FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) | Retrieves the URL of the app's Documents directory | Returns an array; use first element for the path |
| write(to: atomically:) | Writes data to a file at the specified URL | Atomically ensures file integrity during write |
Example: Writing a Text File to Documents Directory
import Foundation
let text = "Hello, World!"
let fileName = "example.txt"
// Get the documents directory URL
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
fatalError("Unable to access documents directory")
}
// Create the file URL
let fileURL = documentsDirectory.appendingPathComponent(fileName)
// Write text to file
do {
try text.write(to: fileURL, atomically: true, encoding: .utf8)
print("File written successfully to \(fileURL.path)")
} catch {
print("Failed to write file: \(error)")
}
Note: The
atomically parameter ensures that the file is written to a temporary location first and then moved into place, preventing partial writes if the app crashes during writing.
6. Using Codable for File Persistence
| Property | Type | Description |
|---|---|---|
| Codable | Protocol | Combines Encodable and Decodable protocols for encoding and decoding data. |
| Encodable | Protocol | Defines the ability to encode an instance into a data format (e.g., JSON). |
| Decodable | Protocol | Defines the ability to decode data into an instance. |
Example: Saving Codable Data to File
import Foundation
struct User: Codable {
let name: String
let email: String
}
let user = User(name: "John Doe", email: "john@example.com")
let encoder = JSONEncoder()
if let data = try? encoder.encode(user) {
let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("user.json")
try? data.write(to: url)
}
Example: Loading Codable Data from File
import Foundation
struct User: Codable {
let name: String
let email: String
}
let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("user.json")
if let data = try? Data(contentsOf: url),
let user = try? JSONDecoder().decode(User.self, from: data) {
print("Loaded user: \"\(user.name)\"")
}
Note: Ensure that the data model conforms to
Encodable and Decodable for seamless serialization/deserialization.
7. Understanding App Sandbox Directories
| Directory | Description | Access Level | Use Case |
|---|---|---|---|
| Documents | Stores user-generated content and files that should be backed up | Read/Write | User documents, saved projects, configuration files |
| Library | Contains app data that should not be backed up (e.g., caches, preferences) | Read/Write | Cached data, preferences, app-specific settings |
| tmp | Temporary files that can be deleted at any time by the system | Read/Write | Temporary files, intermediate processing data |
| Library/Caches | Stores cached data that can be recreated if deleted | Read/Write | App cache, downloaded content, temporary files |
| Library/Preferences | Stores user preferences and app settings | Read/Write | User preferences, app configuration |
Example: Accessing Sandbox Directories
import Foundation
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let libraryPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)[0]
let tmpPath = NSTemporaryDirectory()
print("Documents Path: \(documentsPath)")
print("Library Path: \(libraryPath)")
print("Temporary Path: \(tmpPath)")
8. Using Keychain for Secure Storage
| Property | Description | Notes |
|---|---|---|
| SecKeychainItemAdd | Adds a new keychain item | Used to store sensitive data securely |
| SecKeychainItemUpdate | Updates an existing keychain item | Allows modification of stored data |
| SecKeychainItemDelete | Deletes a keychain item | Removes sensitive data from storage |
Example: Storing a Password in Keychain
import Security
let password = "mySecretPassword"
let service = "MyAppService"
let account = "user@example.com"
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecValueData as String: password.data(using: .utf8)!
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecSuccess {
print("Password stored successfully")
} else {
print("Failed to store password: \(status)")
}
Example: Retrieving a Password from Keychain
import Security
let service = "MyAppService"
let account = "user@example.com"
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess {
if let data = result as? Data,
let password = String(data: data, encoding: .utf8) {
print("Retrieved password: \(password)")
}
} else {
print("Failed to retrieve password: \(status)")
}
Note: Keychain is designed for storing sensitive data like passwords, tokens, and certificates. It's encrypted at rest and provides secure storage even if the device is compromised.
Warning: Never store passwords in plain text or UserDefaults. Keychain is the recommended approach for sensitive data.
9. Implementing Data Migration
Data migration is the process of moving data from one version of an app to another when schema changes occur. This ensures that users can seamlessly upgrade their apps without losing data.
| Migration Type | Description | Implementation |
|---|---|---|
| Lightweight Migration | Automatically handles simple schema changes like adding optional properties or changing property types. | |
| Custom Migration | Used for complex schema changes that require custom logic to transform data. | |
Example: Custom Migration with Mapping Model
let mappingModel = NSMappingModel.from(
sourceModel: oldModel,
destinationModel: newModel
)
let migrationManager = NSMigrationManager(sourceModel: oldModel, destinationModel: newModel)
try migrationManager.migrateStore(
from: storeURL,
sourceType: NSSQLiteStoreType,
to: newStoreURL,
destinationType: NSSQLiteStoreType,
options: nil
)
Note: Always test migrations with real user data to ensure no data loss or corruption occurs.
Warning: Custom migrations can be complex and should only be used when lightweight migration is insufficient.
10. Understanding iCloud Storage Options
| Storage Type | Description | Limitations | Use Case |
|---|---|---|---|
| iCloud Drive | Stores files in the user's iCloud Drive folder, accessible across devices. | 1GB default storage (can be increased via iCloud+) | Documents, backups, media files |
| Key-Value Storage | Stores small amounts of data using NSUserDefaults or UserDefaults. | Up to 1MB total storage | User preferences, settings, small data |
| Core Data with iCloud | Syncs Core Data stores across devices using iCloud. | Requires proper setup and conflict resolution | App data that needs to sync across devices |
| CloudKit | Full-featured database service for storing and syncing data across devices. | 1GB free storage, paid plans available | Complex data models, user-generated content, real-time collaboration |
Example: Using UserDefaults for Key-Value Storage
import Foundation
let userDefaults = UserDefaults.standard
userDefaults.set("John Doe", forKey: "username")
userDefaults.set(25, forKey: "age")
userDefaults.synchronize()
Note: Key-Value Storage is not suitable for large or complex data structures. For such cases, consider using Core Data or CloudKit.