APIs & Data Management

Chapter 8
Local Storage and Data Selection

Certificate in IOS Development
APIs & Data Management   •   Certificate

Local Storage and Data Selection

Introduction

An iOS application frequently needs to preserve information after a view disappears, a process terminates, or the device is restarted. Examples include a user’s preferred appearance, an authentication credential, downloaded images, a shopping basket, and thousands of structured records. These examples appear similar because all involve “saving data,” but they have fundamentally different requirements.

The central engineering task is therefore not simply to write data to disk. It is to select a storage mechanism whose security, structure, capacity, persistence, performance, synchronization, and backup characteristics match the data. A suitable choice improves reliability and user trust; an unsuitable choice may expose credentials, lose important records, slow the interface, or create inconsistent application state.

This chapter examines five principal local-storage options in iOS development: UserDefaults, Keychain, files, caches, and databases. It also introduces a selection framework, implementation patterns using Swift, lifecycle and backup considerations, and an applied case study.

Learning Objectives

By the end of this chapter, you should be able to:

  • distinguish preferences, secrets, documents, temporary data, and structured records;
  • select UserDefaults, Keychain, files, caches, or a database appropriately;
  • implement basic local persistence using Swift and Foundation APIs;
  • explain protection, backup, atomicity, and performance considerations;
  • avoid common security and data-integrity mistakes; and
  • design a local-storage strategy for a realistic iOS application.

1. The Local-Storage Decision

A storage decision should begin with the characteristics of the data rather than with familiarity with an API. Important questions include:

  1. How sensitive is the data? Is it public, personal, confidential, or an authentication secret?
  2. What is its structure? Is it a single scalar value, a small object, a document, binary media, or a collection of related records?
  3. How large is it? Small values and large media have different performance implications.
  4. How long must it persist? Some data must survive reinstall-independent app launches; other data may be recreated at any time.
  5. Must it be backed up? User-created documents may need backup, while downloaded media can normally be recreated.
  6. How often is it read or written? Frequent writes require careful attention to I/O and concurrency.
  7. Does it require queries or relationships? Filtering, sorting, uniqueness, and relationships generally indicate a database.
  8. Who owns the data? Application state, user content, credentials, and server cache have different lifecycle rules.

A useful principle is that storage technology should follow data semantics. UserDefaults is not a general database, and a database is not automatically the right place for a single Boolean preference.

A storage-selection model

The following model summarizes the principal choices:

Data characteristicAppropriate primary optionTypical examples
Small, non-sensitive preferencesUserDefaultsTheme, first-launch flag, sort choice
Secrets and credentialsKeychainTokens, passwords, encryption keys
User-owned documents or app filesFilesJSON export, PDF, recorded audio
Re-creatable temporary contentCachesImage thumbnails, API responses
Many structured records and queriesDatabaseMessages, tasks, products, transactions

These categories can coexist. For example, a finance application may store a server token in Keychain, a display preference in UserDefaults, transaction records in a database, and downloaded statements as files.

2. Persistence, Protection, and Backup

Persistence

Persistence means that data remains available beyond the lifetime of the object or process that created it. Persistence is not identical to permanence. An application may persist data locally while still allowing the operating system, the user, or the application to remove it.

A robust design defines the expected lifecycle explicitly:

  • Session data exists only while the application is running.
  • Preferences persist across launches but are usually replaceable.
  • User documents should remain until the user deletes them.
  • Caches may be deleted and regenerated.
  • Credentials persist securely but may be invalidated by logout or server policy.

Data protection

iOS provides file-level data protection, which encrypts files on the device under conditions determined by the selected protection class. Developers should select an appropriate protection level rather than assuming that every file has identical availability and security.

A common policy is to make sensitive files inaccessible while the device is locked. However, an application that must process data in the background may require a different policy. The correct choice depends on the threat model and operational requirements.

The Keychain provides a protected storage service designed for small secrets. It is not merely a file with a hidden name. Keychain items have attributes such as service, account, accessibility, and access-control requirements. Developers should avoid storing large payloads in Keychain.

Backup and restoration

Backup policy must be intentional. User-created content normally belongs in backup, whereas a downloaded cache can be excluded because it can be retrieved again. Credentials may require special handling and should never be assumed to behave like ordinary documents across devices or restorations.

A useful classification is:

  • Back up: irreplaceable user-created data.
  • Do not back up: reproducible downloads and generated thumbnails.
  • Protect strongly: credentials, tokens, and cryptographic material.
  • Revalidate after restoration: server-derived data whose authorization or freshness may have changed.

3. UserDefaults

UserDefaults is a persistent key-value store intended for small, non-sensitive settings. It supports property-list-compatible values, including strings, numbers, Booleans, dates, data values, arrays, and dictionaries whose contents also satisfy property-list constraints.

Appropriate uses

Suitable values include:

  • preferred language or appearance mode;
  • whether onboarding has been completed;
  • the selected tab or sort order;
  • a small numeric setting; and
  • a version number used for migration logic.

UserDefaults is not appropriate for passwords, access tokens, large media, rapidly changing records, or complex relational data.

Basic implementation

let defaults = UserDefaults.standard

defaults.set(true, forKey: "hasCompletedOnboarding")
let completed = defaults.bool(forKey: "hasCompletedOnboarding")

For safer code, centralize keys and expose typed properties:

enum PreferenceKey {
    static let appearance = "appearance"
    static let launchCount = "launchCount"
}

struct Preferences {
    private let defaults: UserDefaults

    init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
    }

    var appearance: String {
        get { defaults.string(forKey: PreferenceKey.appearance) ?? "system" }
        set { defaults.set(newValue, forKey: PreferenceKey.appearance) }
    }

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

Dependency injection makes the type testable because a test suite can use a separate UserDefaults suite rather than modifying the real application settings.

Limitations

UserDefaults does not provide the query model, indexing, relationships, or transaction semantics expected from a database. It is also a poor location for a large encoded object. Although an object can be encoded as Data, doing so can cause the entire object to be read, decoded, modified, and rewritten whenever one field changes.

The system may synchronize UserDefaults between processes in ways that do not replace the need for a coherent application design. Developers should not use it as a general inter-process coordination mechanism.

4. Keychain

The Keychain is intended for small, sensitive values. Common examples are OAuth refresh tokens, API credentials, private keys, and encryption keys.

Keychain item design

A Keychain item is identified through attributes such as a service and account. The accessibility setting controls when the item may be accessed. For example, an item may be available only after the user has unlocked the device, or it may be made available after first unlock to support certain background operations.

The strongest practical policy is not always the most restrictive policy. If an application genuinely needs a token while the device is locked, choosing a “when unlocked” policy could cause background work to fail. Security requirements and functional requirements must therefore be considered together.

A small wrapper

import Security

struct KeychainStore {
    let service: String

    func save(_ value: Data, account: String) throws {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account,
            kSecValueData as String: value,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        ]

        SecItemDelete(query as CFDictionary)
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else { throw KeychainError(status) }
    }

    func read(account: String) throws -> Data? {
        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: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        if status == errSecItemNotFound { return nil }
        guard status == errSecSuccess else { throw KeychainError(status) }
        return result as? Data
    }
}

enum KeychainError: Error {
    case status(OSStatus)
}

A production wrapper should handle update operations, deletion, access groups where appropriate, and access-control requirements. It should also avoid logging secret values or exposing them in crash reports.

Keychain mistakes

Common errors include storing a token in UserDefaults, placing a complete user profile in Keychain, ignoring Keychain status codes, and assuming that deleting the application always has the desired effect on Keychain items. Keychain lifecycle behavior should be tested on the target deployment environment.

5. Files and Documents

The file system is appropriate for documents and opaque data that are naturally represented as complete files. Examples include exported JSON, downloaded PDF files, images, audio, and locally generated reports.

The standard directories have different meanings. The Documents directory is generally for user-visible or user-generated content that should persist. Library locations are commonly used for application support data and caches. Temporary directories are for data that can disappear quickly.

Writing an atomic JSON file

struct Profile: Codable {
    let displayName: String
    let email: String
}

let profile = Profile(displayName: "Amina", email: "[email protected]")
let encoder = JSONEncoder()
let data = try encoder.encode(profile)

let directory = try FileManager.default.url(
    for: .applicationSupportDirectory,
    in: .userDomainMask,
    appropriateFor: nil,
    create: true
)

let fileURL = directory.appendingPathComponent("profile.json")
try data.write(to: fileURL, options: [.atomic])

An atomic write generally creates a replacement and moves it into place, reducing the risk that a crash leaves a partially written file. Atomicity of one file does not create a transaction across several files. If multiple files must change consistently, use a database transaction or a deliberate journaling strategy.

File protection and exclusion

File attributes can specify data-protection behavior. Re-creatable files should also be excluded from backup where appropriate. These decisions should be documented because they affect user privacy, storage consumption, and restoration behavior.

Files work well when the application usually reads or writes the whole object. They become less suitable when the application must frequently update individual records, enforce uniqueness, sort large collections, or support concurrent access.

6. Caches

A cache stores data that can be regenerated from another authoritative source. The source might be a server, a bundled resource, or a computation. A cache is an optimization, not the system of record.

Typical cached data includes image thumbnails, API responses, search suggestions, and compiled intermediate data. Cache contents may be removed when storage is constrained, so application correctness must not depend exclusively on them.

A cache design should define:

  • the cache key and namespace;
  • expiration or freshness rules;
  • maximum size or eviction policy;
  • behavior after a cache miss; and
  • whether cached content contains personal or sensitive information.

The system-provided caches directory communicates the intended lifecycle. Developers should not place irreplaceable documents there. A cache may improve responsiveness, but it must not cause an application to display stale or unauthorized information indefinitely.

7. Databases and Structured Data

A database is appropriate when data consists of many records and requires querying, sorting, relationships, uniqueness constraints, or incremental updates. iOS applications may use technologies such as Core Data, SwiftData, SQLite, or an SQLite-based library. The choice depends on project requirements, deployment targets, team expertise, and interoperability needs.

Database characteristics

A database can provide:

  • structured schemas;
  • indexes for faster lookup;
  • transactions for all-or-nothing changes;
  • relationships between entities;
  • predicates and sorting; and
  • migration mechanisms as the model evolves.

A database is not automatically faster than a file. For a small object, encoding one file may be simpler and faster. Database benefits become more significant as record count, query complexity, concurrent access, and update granularity increase.

Transactional thinking

Suppose an application moves money between two local account records. The debit and credit must be treated as one logical operation. If the debit succeeds and the credit fails, local data is inconsistent. A transaction ensures that either both changes are committed or neither is.

Transactions are especially valuable for operations involving several records, uniqueness rules, inventory counts, or synchronization metadata. They do not, by themselves, guarantee that a remote server and local database change simultaneously; distributed synchronization requires additional protocol design.

Core Data and SwiftData

Core Data is an object-graph and persistence framework that can use SQLite as a store. It provides managed objects, contexts, predicates, change tracking, and migration support. SwiftData offers a more Swift-oriented model declaration for supported platform versions. Neither framework eliminates the need to understand model versions, relationships, fetch costs, and concurrency.

For certificate-level applications, the key design lesson is to keep persistence models deliberate. Avoid loading an entire large table into memory when a fetch can be filtered and limited. Define indexes for frequent lookup fields where the chosen framework supports them, and perform persistence work away from the main thread when operations may be expensive.

8. Performance and Concurrency

Local storage affects interface responsiveness because disk access, decoding, database queries, and encryption can consume time. User-interface work should remain on the main actor, while potentially expensive persistence work should be isolated appropriately.

Performance is influenced by:

  • amount of data transferred;
  • number of file operations;
  • serialization and deserialization cost;
  • query selectivity and indexing;
  • database transaction size; and
  • frequency of writes.

A common mistake is saving on every keystroke. A better approach may be debouncing writes, saving at meaningful checkpoints, or using a database designed for incremental changes. However, delayed persistence creates a risk of data loss, so the acceptable interval must be based on the importance of the data.

Concurrency also creates correctness risks. Two tasks may read the same value, modify it independently, and overwrite one another. Use actor isolation, serialized access, database transactions, or another explicit coordination mechanism. “It is local” does not mean “it is free from race conditions.”

9. Data Modeling and Migration

Persistence outlives the code version that created it. Therefore, every persistent model should have an evolution strategy.

A migration may involve:

  • adding a field with a safe default;
  • renaming a field while preserving existing values;
  • converting an old representation into a new one;
  • removing obsolete records; or
  • rebuilding a derived cache.

Use version identifiers for encoded files and database schemas. Treat decoding failure as a designed condition: the application may migrate, recover from a backup, request fresh server data, or present an appropriate error. It should not silently discard irreplaceable user content.

A useful distinction is between authoritative and derived data. Authoritative local data represents user work and must be preserved. Derived data can be reconstructed. This distinction influences migration, backup, cache eviction, and error recovery.

10. Security, Privacy, and Governance

Local data may be exposed through device compromise, backups, debugging artifacts, screenshots, logs, analytics, or poorly controlled sharing features. A secure implementation therefore considers the complete data path.

Best practices include:

  • apply data minimization: store only what the application needs;
  • use Keychain for secrets;
  • avoid placing credentials in logs or UserDefaults;
  • apply file protection appropriate to the use case;
  • encrypt especially sensitive user content when the threat model requires it;
  • remove data on logout when policy requires removal;
  • define retention and deletion behavior;
  • validate data before decoding or importing it; and
  • document backup and export implications.

Privacy regulations may require transparency, purpose limitation, access, correction, and deletion. Although a certificate developer may not design the organization’s legal policy, the developer must implement the technical consequences of that policy accurately.

11. Applied Case: TrailTrack Hiking Application

The following is an illustrative case study.

Background

TrailTrack allows users to record hikes, view maps, download trail information, and synchronize completed hikes with a server. It supports offline use and stores a large number of route points.

Problem

The initial prototype stores every value in one encoded dictionary in UserDefaults. Users report slow launches, lost route updates after crashes, and concern that the authentication token is exposed in application settings. Downloaded map tiles also consume backup space.

Analysis

The data has four distinct categories:

DataSensitivityStructureRe-creatable?Recommended storage
Map appearance preferenceLowSingle valueYesUserDefaults
Refresh tokenHighSmall secretNo, immediatelyKeychain
Completed hike and route pointsPersonalMany related recordsUsually noDatabase
Downloaded map tilesPotentially personal, but reproducibleBinary filesYesCaches

The prototype failed because it selected storage based on implementation convenience rather than data characteristics.

Approach and solution

The team creates a typed preferences object for map style and measurement units. The refresh token moves to Keychain with an accessibility policy compatible with the application’s background synchronization requirements. Hikes become database entities related to route-point records. Database transactions are used when a hike is finalized so that metadata and route points are committed consistently.

Map tiles are stored in the caches directory with keys based on trail region and tile version. When a tile is missing, the application downloads it again. The cache is excluded from backup. User-created hike data is stored in an application-support location managed by the database and included in the application’s intended backup policy.

Outcome

Launch time improves because the application no longer decodes the entire hiking history at startup. A failed upload does not delete the local hike. The token is removed from ordinary preference data, and cache growth can be controlled without risking user records.

Learning points

  1. A single application commonly needs several storage mechanisms.
  2. Re-creatable data should not be treated as a permanent document.
  3. Related records and incremental updates favor a database.
  4. Credentials require a security-focused API.
  5. Performance, backup, privacy, and lifecycle decisions are interconnected.

12. A Practical Selection Procedure

When designing a feature, use the following sequence:

  1. List every persistent value separately.
  2. Classify each value as preference, secret, document, cache, or structured record.
  3. Identify the authoritative source and whether the value can be regenerated.
  4. Define sensitivity and required protection while locked or unlocked.
  5. Estimate size, growth, read frequency, and write frequency.
  6. Determine whether queries, relationships, uniqueness, and transactions are required.
  7. Decide whether the data belongs in backup.
  8. Choose the smallest suitable technology.
  9. Define error handling, migration, deletion, and recovery behavior.
  10. Test interruption, device locking, restoration, low storage, concurrent access, and schema changes.

The phrase “smallest suitable technology” is important. Simplicity reduces defects, but oversimplification creates later migration problems. A file may be ideal for a complete document; it is not ideal merely because it is easy to code.

Challenges and Limitations

No local-storage mechanism solves every problem. UserDefaults is convenient but weak for structure and sensitive content. Keychain is secure for secrets but unsuitable for large collections and may have access constraints. Files are transparent and flexible but require developers to design indexing, consistency, and migration. Caches improve speed but may disappear. Databases support sophisticated data operations but introduce schema, migration, concurrency, and testing complexity.

Local storage also creates synchronization challenges. If the same record exists locally and remotely, conflict resolution must be defined. Last-write-wins may be acceptable for a display preference but dangerous for financial or collaborative data. Offline-first applications must distinguish pending operations, confirmed server state, and failed synchronization.

Contemporary Developments

Modern Swift development increasingly emphasizes structured concurrency, actor isolation, typed persistence models, and observation-driven interfaces. These features can improve clarity but do not replace storage fundamentals. A declarative data layer still requires careful decisions about schema, migration, caching, and privacy.

AI-enabled applications add further considerations. Prompts, conversation histories, embeddings, and generated outputs may contain personal or confidential information. Developers should classify such data before storing it locally, define retention limits, and avoid sending local content to an external AI service without appropriate consent and governance. A vector index or embedding store may be useful for retrieval, but it remains a derived data structure and should not replace the authoritative source.

Best-Practice Checklist

  • Use UserDefaults only for small, non-sensitive preferences.
  • Use Keychain for tokens, passwords, and cryptographic secrets.
  • Use files for complete documents and binary assets.
  • Use caches only for data that can be recreated.
  • Use a database for collections requiring queries, relationships, or transactions.
  • Select file-protection and backup behavior deliberately.
  • Keep persistence work off the main interface path when it may be expensive.
  • Centralize storage APIs instead of scattering keys throughout the application.
  • Test migration, interruption, deletion, device locking, and low-storage conditions.
  • Do not log secrets or assume local data is harmless.

Chapter Summary

Local storage is a design problem involving sensitivity, structure, size, persistence, performance, backup, and lifecycle. UserDefaults is suitable for small, non-sensitive preferences. Keychain is designed for small secrets and credentials. Files are suitable for documents and opaque binary data. Caches hold re-creatable content and may be deleted. Databases support structured collections, queries, relationships, incremental updates, and transactions.

A reliable iOS application often combines these mechanisms rather than selecting one universal store. Correct design also requires data-protection choices, backup classification, concurrency control, migration planning, privacy governance, and recovery behavior. The most important professional habit is to classify each value before choosing its storage technology.

Key Terms

UserDefaults: A persistent key-value store for small, non-sensitive application preferences.

Keychain: A protected system service for storing small secrets and credentials.

File protection: iOS controls that regulate the accessibility and encryption of files.

Document: User-owned or application-generated content intended to persist.

Cache: Re-creatable data stored to improve performance.

Database: A structured persistence system supporting records, queries, and often transactions.

Schema: The structural definition of stored data.

Migration: The controlled conversion of persisted data to a newer model.

Transaction: A group of database operations committed as one logical unit.

Atomic write: A write strategy that replaces a file in a way that reduces partial-write risk.

Backup: A copy of data retained for restoration.

Data minimization: Storing only data necessary for a defined purpose.

Persistence: Retention of data beyond an object or process lifetime.

Authoritative data: The trusted source from which other representations may be derived.

Derived data: Data that can be reconstructed from another source.

Concurrency: Multiple tasks accessing or changing data during overlapping periods.

Serialization: Conversion of structured values into a storable representation.

Eviction: Removal of cache data according to a policy.

Data protection class: A policy controlling when protected file data can be accessed.

Conflict resolution: Rules for reconciling differing local and remote versions.

Review Questions

Short Answer Questions

  1. What type of information is UserDefaults designed to store?
  2. Why should an authentication token generally be stored in Keychain rather than UserDefaults?
  3. What distinguishes a cache from an authoritative document?
  4. Give two characteristics that indicate a database may be appropriate.
  5. What is the purpose of an atomic file write?

Long Answer Questions

  1. Compare UserDefaults, Keychain, files, caches, and databases in terms of structure, sensitivity, persistence, and typical use.
  2. Explain how backup and file-protection decisions affect the design of local storage.
  3. Discuss why a database transaction is important when several related records must change together.
  4. Describe a migration strategy for an encoded local document whose model has changed.
  5. Explain how concurrency and main-thread performance should influence persistence design in an iOS application.

Analytical / Application Questions

  1. An application stores a user’s profile, access token, downloaded images, and thousands of messages. Select a suitable storage mechanism for each item and justify every decision.
  2. A cache contains private search results. Analyze the privacy and lifecycle risks and propose controls for backup, expiration, deletion, and logging.
  3. Design a local-storage strategy for an offline shopping application that must preserve a cart, store credentials securely, cache product images, and synchronize orders after connectivity returns.

Multiple Choice Questions with Answers

Question 1

Which type of data is most appropriate for UserDefaults?

A. An OAuth refresh token

B. A user’s preferred appearance mode

C. Thousands of related financial records

D. A large video file

Correct Answer: B. A user’s preferred appearance mode

Question 2

Which iOS facility is intended primarily for small secrets such as passwords and tokens?

A. Caches directory

B. UserDefaults

C. Keychain

D. Temporary directory

Correct Answer: C. Keychain

Question 3

What is the defining characteristic of cache data?

A. It must be preserved as the authoritative user record

B. It can be regenerated from another source

C. It must always be encrypted with a database key

D. It must contain relational tables

Correct Answer: B. It can be regenerated from another source

Question 4

Which requirement most strongly indicates the use of a database?

A. Storing one Boolean preference

B. Storing one API token

C. Querying and sorting thousands of related records

D. Saving a single exported PDF

Correct Answer: C. Querying and sorting thousands of related records

Question 5

What is the primary benefit of an atomic file write?

A. It automatically encrypts the file

B. It prevents all concurrent access

C. It reduces the risk of leaving a partially written file

D. It guarantees synchronization with a server

Correct Answer: C. It reduces the risk of leaving a partially written file

Question 6

Which data should generally be excluded from backup when it can easily be downloaded again?

A. An irreplaceable user document

B. A completed legal form created by the user

C. A downloaded image thumbnail cache

D. A user’s locally authored notes

Correct Answer: C. A downloaded image thumbnail cache

Question 7

Why should a database transaction be used when finalizing a hike and its route points?

A. To make the records public

B. To ensure related changes commit as one logical operation

C. To convert the records into UserDefaults values

D. To guarantee that the server accepts the upload

Correct Answer: B. To ensure related changes commit as one logical operation

Question 8

Which is a poor use of UserDefaults?

A. Storing a selected sort order

B. Storing whether onboarding is complete

C. Storing a password

D. Storing a small integer preference

Correct Answer: C. Storing a password

Question 9

What should an application do if a cache entry is missing?

A. Treat the cache as permanently authoritative

B. Regenerate or retrieve the data from its source

C. Store the missing value in Keychain

D. Terminate immediately

Correct Answer: B. Regenerate or retrieve the data from its source

Question 10

Which question should be asked before selecting a local-storage mechanism?

A. Which API has the shortest name?

B. Can the data be classified by sensitivity, structure, size, and lifecycle?

C. Can every value be stored in one dictionary?

D. Will the data ever be displayed in a table?

Correct Answer: B. Can the data be classified by sensitivity, structure, size, and lifecycle?

APIs & Data Management
Chapter 8 of 14