swift-actor-persistence について
Swift 開発者向けのスレッドセーフなローカル永続化パターンです。Actor によるアクセスとメモリキャッシュ、ファイル保存を組み合わせ、オフライン優先アプリにも対応します。
主な機能
- 01Actor ベースのリポジトリパターン
- 02メモリキャッシュとファイル保存
- 03ID による高速検索
- 04アトミックなファイル書き込み
- 05Codable と Identifiable のモデル対応
活用シーン
- 01ユーザーデータや設定のローカル保存
- 02オフライン優先データレイヤーの構築
- 03並行アクセスされる共有状態の保護
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ xu-xiang/ everything-claude-code-zh --skill swift-actor-persistenceインストール後、Claude Code で/swift-actor-persistenceを入力して読み込まれたか確認します。
詳細
Swift Actor を使ったスレッドセーフな永続化
Swift actor を使って、スレッドセーフなデータ永続化レイヤーを構築するパターンです。このパターンでは、メモリーキャッシュとファイルベースのストレージを組み合わせ、actor モデルによってデータ競合をコンパイル時に排除します。
有効にするタイミング
- Swift 5.5+ でデータ永続化レイヤーを構築する場合
- 共有可変状態にスレッドセーフにアクセスする必要がある場合
- ロックや
DispatchQueuesなどの手動同期操作をなくしたい場合 - ローカルストレージを備えたオフラインファーストアプリを構築する場合
コアパターン
Actor ベースのリポジトリパターン
actor モデルは、コンパイラーによって強制されるシリアル化されたアクセスを保証し、データ競合が発生しないようにします。
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)
// 在 init 期间同步加载(此时 actor 隔离尚未生效)
self.cache = Self.loadSynchronously(from: fileURL)
}
// MARK: - 公共 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 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(uniqueKeysWithValues: items.map { ($0.id, $0) })
}
}
使い方
actor の隔離により、すべての呼び出しは自動的に非同期になります。
let repository = LocalRepository<Question>()
// 读取 —— 从内存缓存中进行快速 O(1) 查找
let question = await repository.find(by: "q-001")
let allQuestions = await repository.loadAll()
// 写入 —— 原子化地更新缓存并持久化到文件
try await repository.save(newQuestion)
try await repository.delete("q-001")
@Observable ViewModel との組み合わせ
@Observable
final 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()
}
}
重要な設計上の決定
| 決定 | 理由 |
|---|---|
| Actor を使用する(class + lock ではなく) | コンパイラーがスレッドセーフを強制するため、手動同期が不要です |
| メモリーキャッシュ + ファイル永続化 | キャッシュによって高速に読み取れ、ディスクによって書き込みが永続化されます |
| init で同期的に読み込む | 非同期初期化の複雑さを避けられ、ローカルファイルへの影響も小さいためです |
| ID をキーとする辞書 | 識別子による O(1) 検索が可能です |
ジェネリック型で Codable & Identifiable をサポートする | あらゆるモデル型で再利用できます |
ファイルをアトミックに書き込む(.atomic) | クラッシュによる書き込みの部分的な失敗でデータが破損するのを防ぎます |
ベストプラクティス
- actor 境界を越えるすべてのデータには
Sendable型を使用します - actor の公開 API は最小限に保ちます —— 永続化の詳細ではなく、ドメインの操作のみを公開します
.atomic書き込みを使用して、書き込み中にアプリがクラッシュした場合のデータ破損を防ぎますinitで同期的に読み込みます —— 非同期初期化は複雑さを増す一方、ローカルファイルの同期読み込みには大きな利点があります@ObservableViewModel と組み合わせて、リアクティブな UI 更新を実現します
避けるべきアンチパターン
- 新しい Swift 並行処理コードで actor ではなく
DispatchQueueやNSLockを使用する - 内部キャッシュ辞書を外部の呼び出し元に直接公開する
- 検証せずにファイル URL を設定可能にする
- すべての actor メソッド呼び出しに
awaitが必要であることを忘れる —— 呼び出し元は非同期コンテキストに対応する必要があります nonisolatedを使って actor の隔離を回避する(actor を使う本来の目的に反します)
使用する場面
- iOS/macOS アプリのローカルデータストレージ(ユーザーデータ、設定、キャッシュされたコンテンツ)
- 後でサーバーと同期するオフラインファーストアーキテクチャ
- アプリの複数の部分から並行してアクセスされる共有可変状態全般
DispatchQueueベースの従来のスレッドセーフ方式を、最新の Swift 並行処理に置き換える場合
コメント
Markdown 対応