Skip to content

Swift Data Persistence — UserDefaults, Core Data, SwiftData, and Keychain

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Swift Data Persistence. We cover key concepts, practical examples, and best practices to help you master this topic.

Swift data persistence encompasses several technologies that allow iOS and macOS apps to save data between launches, from simple key-value storage with UserDefaults to relational databases with Core Data and the modern SwiftData framework.

What You'll Learn

  • UserDefaults for simple key-value storage
  • Codable-based JSON file persistence
  • Core Data fundamentals and stack setup
  • SwiftData (iOS 17+) for modern persistence
  • Keychain for secure credential storage
  • Choosing the right persistence layer

Why It Matters

Without persistence, all app data is lost when the app closes. User preferences, login tokens, cached API responses, and user-generated content must survive app restarts and device reboots. Choosing the correct persistence Strategy directly affects app performance, security, and user experience.

Real-World Use

A note-taking app uses SwiftData for storing notes with full-text search, UserDefaults for user preferences (font size, theme), and the Keychain for storing the user's authentication token. Each persistence technology handles a specific need at the appropriate level of complexity.

Learning Path

flowchart LR
  A[Networking
Lesson 20] --> B[Data Persistence
You are here] B --> C[Concurrency
Lesson 22] B --> D[Notifications
Lesson 23] style B fill:#f90,color:#fff

UserDefaults

UserDefaults stores simple key-value pairs: strings, numbers, booleans, dates, and Data objects. It is fast and thread-safe but designed for small amounts of data only.

import Foundation

class SettingsManager {
    static let shared = SettingsManager()

    private let defaults = UserDefaults.standard

    var username: String {
        get { defaults.string(forKey: "username") ?? "Guest" }
        set { defaults.set(newValue, forKey: "username") }
    }

    var isDarkMode: Bool {
        get { defaults.bool(forKey: "darkMode") }
        set { defaults.set(newValue, forKey: "darkMode") }
    }

    var fontSize: Double {
        get {
            let value = defaults.double(forKey: "fontSize")
            return value == 0 ? 14.0 : value
        }
        set { defaults.set(newValue, forKey: "fontSize") }
    }

    var launchCount: Int {
        get { defaults.integer(forKey: "launchCount") }
        set { defaults.set(newValue, forKey: "launchCount") }
    }

    func incrementLaunchCount() {
        launchCount += 1
    }

    func reset() {
        let domain = Bundle.main.bundleIdentifier!
        defaults.removePersistentDomain(forName: domain)
    }
}

SettingsManager.shared.username = "Alice"
SettingsManager.shared.isDarkMode = true
SettingsManager.shared.fontSize = 16.0
SettingsManager.shared.incrementLaunchCount()

print("User: \(SettingsManager.shared.username)")
print("Dark mode: \(SettingsManager.shared.isDarkMode)")
print("Font size: \(SettingsManager.shared.fontSize)")
print("Launches: \(SettingsManager.shared.launchCount)")

Output:

User: Alice
Dark mode: true
Font size: 16.0
Launches: 1

UserDefaults automatically synchronizes to disk periodically. Use it for preferences, settings, and small state flags only.

Codable JSON Persistence

For structured data like arrays of model objects, encode to JSON and save to the app's Documents directory.

import Foundation

struct Task: Codable, Identifiable {
    let id: UUID
    var title: String
    var isCompleted: Bool
    let createdAt: Date
}

class TaskStorage {
    private let fileName = "tasks.json"

    private var fileURL: URL {
        let documents = FileManager.default.urls(
            for: .documentDirectory,
            in: .userDomainMask
        ).first!
        return documents.appendingPathComponent(fileName)
    }

    func save(_ tasks: [Task]) throws {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        let data = try encoder.encode(tasks)
        try data.write(to: fileURL, options: .atomic)
        print("Saved \(tasks.count) tasks to \(fileURL.lastPathComponent)")
    }

    func load() throws -> [Task] {
        guard FileManager.default.fileExists(atPath: fileURL.path) else {
            return []
        }
        let data = try Data(contentsOf: fileURL)
        let decoder = JSONDecoder()
        let tasks = try decoder.decode([Task].self, from: data)
        print("Loaded \(tasks.count) tasks")
        return tasks
    }

    func delete() throws {
        if FileManager.default.fileExists(atPath: fileURL.path) {
            try FileManager.default.removeItem(at: fileURL)
            print("Deleted tasks file")
        }
    }
}

let storage = TaskStorage()
var tasks = [
    Task(id: UUID(), title: "Buy groceries", isCompleted: false, createdAt: Date()),
    Task(id: UUID(), title: "Finish Swift tutorial", isCompleted: true, createdAt: Date())
]

try storage.save(tasks)
let loadedTasks = try storage.load()
for task in loadedTasks {
    let status = task.isCompleted ? "Done" : "Pending"
    print("  \(status): \(task.title)")
}

Output:

Saved 2 tasks to tasks.json
Loaded 2 tasks
  Pending: Buy groceries
  Done: Finish Swift tutorial

This approach works for any Codable type. The data is stored as plain JSON in the app sandbox, making it easy to debug and backup.

Core Data

Core Data is Apple's mature object graph and persistence framework for complex relational data.

import CoreData

class CoreDataStack {
    static let shared = CoreDataStack()

    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "AppModel")
        container.loadPersistentStores { description, error in
            if let error = error {
                fatalError("Core Data failed: \(error.localizedDescription)")
            }
        }
        return container
    }()

    var viewContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }

    func saveContext() {
        let context = viewContext
        if context.hasChanges {
            do {
                try context.save()
                print("Core Data saved")
            } catch {
                print("Save failed: \(error.localizedDescription)")
            }
        }
    }
}

// Creating managed objects requires an .xcdatamodeld file with entity definitions.
// This example assumes a "Person" entity with "name" and "age" attributes.

func createSampleData() {
    let context = CoreDataStack.shared.viewContext

    // In a real project, these would be NSManagedObject subclasses
    // generated from the Core Data model editor:
    // let person = Person(context: context)
    // person.name = "Alice"
    // person.age = 30

    CoreDataStack.shared.saveContext()
    print("Sample data created")
}

func fetchPeople() {
    let context = CoreDataStack.shared.viewContext
    let request = NSFetchRequest<NSManagedObject>(entityName: "Person")

    do {
        let people = try context.fetch(request)
        print("Fetched \(people.count) people")
        for person in people {
            let name = person.value(forKey: "name") as? String ?? ""
            let age = person.value(forKey: "age") as? Int ?? 0
            print("  \(name), age \(age)")
        }
    } catch {
        print("Fetch failed: \(error.localizedDescription)")
    }
}

Core Data requires a data model file (.xcdatamodeld) created in Xcode. It supports relationships, Lazy Loading, faulting, and automatic undo management.

SwiftData

SwiftData is Apple's modern persistence framework introduced in iOS 17. It uses Swift macros for a cleaner API.

import SwiftData
import SwiftUI

// @Model macro generates all necessary Core Data-like code
@Model
class Note {
    var title: String
    var content: String
    var createdAt: Date
    var isFavorite: Bool
    var tags: [String]

    init(title: String, content: String, tags: [String] = []) {
        self.title = title
        self.content = content
        self.createdAt = Date()
        self.isFavorite = false
        self.tags = tags
    }
}

// Usage in SwiftUI:
// struct NotesView: View {
//     @Query(sort: \Note.createdAt, order: .reverse) var notes: [Note]
//     @Environment(\.modelContext) var context
//
//     var body: some View {
//         List {
//             ForEach(notes) { note in
//                 Text(note.title)
//             }
//         }
//         .toolbar {
//             Button("Add") {
//                 let note = Note(title: "New Note", content: "")
//                 context.insert(note)
//             }
//         }
//     }
// }

func swiftDataDemo() {
    let schema = Schema([Note.self])
    let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
    let container = try! ModelContainer(for: Note.self, configurations: config)
    let context = ModelContext(container)

    let note = Note(title: "Hello SwiftData", content: "This is a sample note.")
    context.insert(note)
    try? context.save()

    let descriptor = FetchDescriptor<Note>(
        predicate: #Predicate { $0.isFavorite == true },
        sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
    )

    let favorites = try? context.fetch(descriptor)
    print("Favorites count: \(favorites?.count ?? 0)")
    print("Note saved: \(note.title)")
}

Output: Note saved: Hello SwiftData

SwiftData integrates seamlessly with SwiftUI using @Query and @Model. It eliminates the need for .xcdatamodeld files and manual NSManagedObject subclasses.

Keychain Persistence

The Keychain stores sensitive data like passwords and tokens securely using hardware-backed encryption.

import Foundation
import Security

class KeychainManager {
    static let shared = KeychainManager()

    private let service = "com.dodatech.app"

    func save(key: String, value: String) -> Bool {
        guard let data = value.data(using: .utf8) else { return false }

        // Delete existing item first
        let deleteQuery: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key
        ]
        SecItemDelete(deleteQuery as CFDictionary)

        // Add new item
        let addQuery: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]

        let status = SecItemAdd(addQuery as CFDictionary, nil)
        return status == errSecSuccess
    }

    func read(key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        guard status == errSecSuccess,
              let data = result as? Data,
              let value = String(data: data, encoding: .utf8) else {
            return nil
        }
        return value
    }

    func delete(key: String) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key
        ]
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess
    }
}

let keychain = KeychainManager.shared
let saved = keychain.save(key: "authToken", value: "eyJhbGciOiJIUzI1NiIs...")
print("Token saved: \(saved)")

if let token = keychain.read(key: "authToken") {
    print("Read token: \(token.prefix(20))...")
}

Output:

Token saved: true
Read token: eyJhbGciOiJIUzI1NiIs...

Never store passwords, tokens, or API keys in UserDefaults. The Keychain is the only secure storage option on iOS.

Choosing the Right Persistence Layer

Technology Best For Limitations
UserDefaults Preferences, settings, small flags Not for structured or large data
Codable JSON Structured data, small to medium datasets Loads entire file into memory
Core Data Complex relational data, large datasets Heavy setup, requires model file
SwiftData Modern Swift apps (iOS 17+) Requires iOS 17 or later
Keychain Passwords, tokens, secrets Key-value only, slower access

Common Mistakes

  1. Storing sensitive data in UserDefaults: UserDefaults is plain text in the app sandbox. Never store passwords, tokens, or credit card info there.

  2. Loading large datasets with Codable: JSON decoding loads the entire file into memory. For datasets over a few MB, use Core Data or SQLite.

  3. Forgetting to save Core Data context: Changes in the managed object context are not persisted until save() is called. If the app crashes before saving, changes are lost.

  4. Using SwiftData with older iOS versions: SwiftData requires iOS 17 or later. Check deployment target before adopting it.

  5. Not handling Keychain errors gracefully: Keychain operations can fail due to security policies, device restrictions, or keychain corruption. Always handle the error cases.

Practice Questions

  1. What is the maximum data size appropriate for UserDefaults?
  2. Why should you not store authentication tokens in UserDefaults?
  3. What is the difference between Core Data and SwiftData?
  4. How does the Keychain encrypt data differently from file-based storage?
  5. Challenge: Build a RecentSearches system that persists up to 10 recent search strings using UserDefaults. When the 11th search is added, remove the oldest. Then reimplement it using Codable JSON storage.

Mini Project

Create a JournalApp persistence layer with:

  • A JournalEntry struct with id, title, content, date, mood (enum: happy, neutral, sad)
  • A PersistenceManager class with save(_:), load(), delete(_:), and search(query:) methods
  • Use Codable JSON storage for the entries
  • Use UserDefaults for app preferences (entry font size, sort order)
  • Use Keychain for a simulated "passcode lock" feature
  • Print all entries and settings on launch

FAQ

Can I use UserDefaults for storing arrays or dictionaries?

Yes, UserDefaults supports arrays and dictionaries containing property-list types (String, Int, Data, Date, etc.). For custom model objects, use Codable JSON instead.

What is the file size limit for UserDefaults?

Apple recommends keeping UserDefaults under 512 KB. For larger data, use file-based or database storage.

Is Core Data still relevant with SwiftData?

SwiftData is built on top of Core Data and offers a modern API. Core Data is still fully supported and used in existing projects. Both are valid choices depending on deployment target.

How do I handle Core Data migrations?

Use lightweight migration (automatic) for simple changes like adding attributes. For complex changes, create a mapping model. Test migrations thoroughly before shipping.

Can I encrypt Core Data or JSON files?

Use Data Protection APIs (NSFileProtection) to encrypt files at rest. For individual values, use CommonCrypto or CryptoKit before writing to disk.

What's Next

After mastering persistence, learn how to manage concurrent operations safely with Concurrency, or handle real-time updates with Notifications.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro