Swift Actor Persistence

作者 affaan-mef648e01899b无许可证275K 个星标收录于 2026年10月8日更新于 2026年10月8日仓库3天前更新

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

AI 生成的概览

使用 Swift actor 实现线程安全的本地持久化,结合内存缓存与文件存储。

功能
提供使用 actor 构建线程安全持久化层的 Swift 代码模式,将内存字典缓存与 JSON 文件存储结合。内容包括泛型 actor 仓库及其 save、delete、find、loadAll 操作、@Observable 视图模型示例、设计决策表、最佳实践与反模式。产出为设计指导与示例代码,而非可直接运行的脚本。
适用场景
适用于在 Swift 5.9+ 的 iOS 或 macOS 项目中构建本地数据持久化层、需要对共享可变状态进行线程安全访问、用 actor 取代手动锁或 DispatchQueue 同步,以及构建离线优先的本地存储应用。不适用于非 Swift 项目或服务端数据库工作。
运行要求
不附带脚本或工具,仅为说明性内容。面向 Swift 5.9+ 项目(iOS 17+、macOS 14+),需要熟悉 Swift 并发、Codable 与文件存储 API。

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

来源与署名

来源:affaan-m/ecc位于.kiro/skills/swift-actor-persistence提交ef648e0

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架