Swift Actor Persistence

by affaan-mef648e01899bNo license275K starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 3 days ago

Thread-safe data persistence in Swift using actors — in-memory cache with file-backed storage, eliminating data races by design.

Instructions onlySoftware Development
AI-generated overview

Swift actor patterns for thread-safe local persistence with an in-memory cache and file-backed storage.

What it does
Provides Swift code patterns for building a thread-safe persistence layer using actors, combining an in-memory dictionary cache with JSON file storage. It includes a generic actor repository with save, delete, find, and loadAll operations, an example @Observable view model, a table of design decisions, best practices, and anti-patterns. The deliverable is guidance and example code rather than a runnable script.
When to use it
Use when building a local data persistence layer in Swift 5.9+ for iOS or macOS, when shared mutable state needs thread-safe access, when replacing manual locks or DispatchQueue synchronization with actors, or when building offline-first apps with local storage. It is not intended for non-Swift projects or server-side database work.
Requirements
No scripts or tools are shipped; it is instructions only. It targets Swift 5.9+ projects (iOS 17+, macOS 14+) and assumes familiarity with Swift concurrency, Codable, and file storage APIs.

Swift Actors for Thread-Safe Persistence

Patterns for building thread-safe data persistence layers using Swift actors. Combines in-memory caching with file-backed storage, leveraging the actor model to eliminate data races at compile time.

When to Activate

  • Building a data persistence layer in Swift 5.9+ (iOS 17+, macOS 14+)
  • Need thread-safe access to shared mutable state
  • Want to eliminate manual synchronization (locks, DispatchQueues)
  • Building offline-first apps with local storage

Core Pattern

Actor-Based Repository

The actor model guarantees serialized access — no data races, enforced by the compiler.

swift
public actor LocalRepository<T: Codable & Identifiable> where T.ID == String {    private var cache: [String: T] = [:]    private let fileURL: URL
    public init(directory: URL = .documentsDirectory, filename: String = "data.json") {        self.fileURL = directory.appendingPathComponent(filename)        // Synchronous load during init (actor isolation not yet active)        self.cache = Self.loadSynchronously(from: fileURL)    }
    // MARK: - Public API
    public func save(_ item: T) throws {        cache[item.id] = item        try persistToFile()    }
    public func delete(_ id: String) throws {        cache[id] = nil        try persistToFile()    }
    public func find(by id: String) -> T? {        cache[id]    }
    public func loadAll() -> [T] {        Array(cache.values)    }
    // MARK: - Private
    private func persistToFile() throws {        let data = try JSONEncoder().encode(Array(cache.values))        try data.write(to: fileURL, options: .atomic)    }
    private static func loadSynchronously(from url: URL) -> [String: T] {        guard let data = try? Data(contentsOf: url),              let items = try? JSONDecoder().decode([T].self, from: data) else {            return [:]        }        return Dictionary(items.map { ($0.id, $0) }, uniquingKeysWith: { _, latest in latest })    }}

Usage

All calls are automatically async due to actor isolation:

swift
let repository = LocalRepository<Question>()
// Read — fast O(1) lookup from in-memory cachelet question = await repository.find(by: "q-001")let allQuestions = await repository.loadAll()
// Write — updates cache and persists to file atomicallytry await repository.save(newQuestion)try await repository.delete("q-001")

Combining with @Observable ViewModel

swift
@Observablefinal class QuestionListViewModel {    private(set) var questions: [Question] = []    private let repository: LocalRepository<Question>
    init(repository: LocalRepository<Question> = LocalRepository()) {        self.repository = repository    }
    func load() async {        questions = await repository.loadAll()    }
    func add(_ question: Question) async throws {        try await repository.save(question)        questions = await repository.loadAll()    }}

Key Design Decisions

DecisionRationale
Actor (not class + lock)Compiler-enforced thread safety, no manual synchronization
In-memory cache + file persistenceFast reads from cache, durable writes to disk
Synchronous init loadingAvoids async initialization complexity
Dictionary keyed by IDO(1) lookups by identifier
Generic over Codable & IdentifiableReusable across any model type
Atomic file writes (.atomic)Prevents partial writes on crash

Best Practices

  • Use Sendable types for all data crossing actor boundaries
  • Keep the actor's public API minimal — only expose domain operations, not persistence details
  • Use .atomic writes to prevent data corruption if the app crashes mid-write
  • Load synchronously in init — async initializers add complexity with minimal benefit for local files
  • Combine with @Observable ViewModels for reactive UI updates

Anti-Patterns to Avoid

  • Using DispatchQueue or NSLock instead of actors for new Swift concurrency code
  • Exposing the internal cache dictionary to external callers
  • Making the file URL configurable without validation
  • Forgetting that all actor method calls are await — callers must handle async context
  • Using nonisolated to bypass actor isolation (defeats the purpose)

When to Use

  • Local data storage in iOS/macOS apps (user data, settings, cached content)
  • Offline-first architectures that sync to a server later
  • Any shared mutable state that multiple parts of the app access concurrently
  • Replacing legacy DispatchQueue-based thread safety with modern Swift concurrency

Source and attribution

Source:affaan-m/eccin.kiro/skills/swift-actor-persistenceat commitef648e0

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal