Manan Patel

Tinkering and Sharing

💾 iOS Data Persistence

Core Data, UserDefaults, and other data persistence options in iOS

schedule 7 min read calendar_today January 8, 2024

Data Persistence Options

iOS provides several options for persisting data, each with its own use cases and trade-offs.

1. UserDefaults

Simple key-value storage for user preferences and app settings.

// Storing data
UserDefaults.standard.set("John Doe", forKey: "username")
UserDefaults.standard.set(25, forKey: "age")
UserDefaults.standard.set(true, forKey: "isFirstLaunch")

// Retrieving data
let username = UserDefaults.standard.string(forKey: "username")
let age = UserDefaults.standard.integer(forKey: "age")
let isFirstLaunch = UserDefaults.standard.bool(forKey: "isFirstLaunch")

// Custom UserDefaults wrapper
extension UserDefaults {
    enum Keys {
        static let username = "username"
        static let age = "age"
        static let isFirstLaunch = "isFirstLaunch"
    }
    
    var username: String? {
        get { string(forKey: Keys.username) }
        set { set(newValue, forKey: Keys.username) }
    }
}

2. Core Data

Apple's object graph and persistence framework for complex data models.

// Core Data Stack
class CoreDataStack {
    static let shared = CoreDataStack()
    
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "DataModel")
        container.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Core Data error: \(error)")
            }
        }
        return container
    }()
    
    var context: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
    
    func saveContext() {
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                print("Save error: \(error)")
            }
        }
    }
}

// Using Core Data
class UserManager {
    private let context = CoreDataStack.shared.context
    
    func createUser(name: String, email: String) {
        let user = User(context: context)
        user.name = name
        user.email = email
        user.id = UUID()
        
        CoreDataStack.shared.saveContext()
    }
    
    func fetchUsers() -> [User] {
        let request: NSFetchRequest = User.fetchRequest()
        do {
            return try context.fetch(request)
        } catch {
            print("Fetch error: \(error)")
            return []
        }
    }
}

3. File System

Direct file system access for custom data formats and large files.

class FileManager {
    static let shared = FileManager()
    
    private let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    
    func saveData(_ data: Data, to fileName: String) {
        let fileURL = documentsDirectory.appendingPathComponent(fileName)
        do {
            try data.write(to: fileURL)
        } catch {
            print("Save error: \(error)")
        }
    }
    
    func loadData(from fileName: String) -> Data? {
        let fileURL = documentsDirectory.appendingPathComponent(fileName)
        return try? Data(contentsOf: fileURL)
    }
    
    func saveJSON(_ object: T, to fileName: String) {
        do {
            let data = try JSONEncoder().encode(object)
            saveData(data, to: fileName)
        } catch {
            print("JSON encoding error: \(error)")
        }
    }
    
    func loadJSON(_ type: T.Type, from fileName: String) -> T? {
        guard let data = loadData(from: fileName) else { return nil }
        do {
            return try JSONDecoder().decode(type, from: data)
        } catch {
            print("JSON decoding error: \(error)")
            return nil
        }
    }
}

4. Keychain

Secure storage for sensitive data like passwords and tokens.

import Security

class KeychainManager {
    static let shared = KeychainManager()
    
    private let service = "com.yourapp.keychain"
    
    func save(_ data: Data, for key: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecValueData as String: data
        ]
        
        SecItemDelete(query as CFDictionary)
        SecItemAdd(query as CFDictionary, nil)
    }
    
    func load(for key: String) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        return status == errSecSuccess ? result as? Data : nil
    }
    
    func delete(for key: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key
        ]
        
        SecItemDelete(query as CFDictionary)
    }
}

5. Best Practices

✅ Do's

  • Use UserDefaults for simple preferences
  • Use Core Data for complex relational data
  • Use Keychain for sensitive data
  • Handle errors gracefully
  • Consider data migration strategies

❌ Don'ts

  • Don't store sensitive data in UserDefaults
  • Don't perform heavy operations on the main thread
  • Don't ignore Core Data migration
  • Don't store large files in Core Data