Robust, concurrenc-safe actor-based file storage for Apple platforms.
FileStorage gives you JSON-backed persistence primitives that are safe across concurrent tasks and coordinated across processes (app, extensions, widgets) using NSFileCoordinator. A solid replacement for UserDefaults when bigger object size is needed.
FileObjectStorage<Content>for a single Codable valueFileArrayStorage<Element>for append/trim/sort array workflowsFileDictionaryStorage<Key, Value>for keyed updates and reads- Coordinated access blocks (backed by
NSFileCoordinator) for atomic multi-step updates - App Group storage with fallback to Documents
FileChangeObserverfor file-change notifications viaNSFilePresenter
- Swift tools:
6.2 - iOS
16.0+ - watchOS
9.0+ - macOS
13.0+
Add FileStorage to your Package.swift dependencies and target:
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "YourApp",
dependencies: [
.package(url: "https://github.com/kvaDrug/FileStorage.git", from: "1.0.0")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "FileStorage", package: "FileStorage")
]
)
]
)import FileStoragestruct Session: Codable, Sendable {
let token: String
let userId: String
}
let storage = FileObjectStorage(Session.self, fileName: "session.json", appGroupId: "group.com.example.shared")
try await storage.save(Session(token: "abc", userId: "42"))
let session = try await storage.get()Supports max size and built-in sort on append. Ideal for logs.
struct LogItem: Codable, Sendable {
let timestamp: Date
let message: String
}
let logs = FileArrayStorage<LogItem>(
fileName: "logs/events.json",
appGroupId: "group.com.example.shared",
maxSize: 500
)
try await logs.append(LogItem(timestamp: .now, message: "App started"))
// Keep newest first
try await logs.append(
LogItem(timestamp: .now, message: "Another event"),
sorter: { $0.timestamp > $1.timestamp }
)
let allLogs = try await logs.getAll()let dict = FileDictionaryStorage<String, Int>(
fileName: "counters.json",
appGroupId: "group.com.example.shared"
)
try await dict.set("launchCount", value: 1)
try await dict.setMany(["syncCount": 12, "errorCount": 0])
let value = try await dict.get("syncCount")Use coordinatedAccess when you need multiple operations to happen atomically in one coordinated section:
try await logs.coordinatedAccess { url in
var items = try logs.getAll(coordinatedURL: url) ?? []
items.append(.init(timestamp: .now, message: "Atomic write"))
try logs.replace(Array(items.prefix(logs.maxSize)), coordinatedURL: url)
}import Combine
import FileStorage
let observer = FileChangeObserver(
url: dict.fileURL,
publisherQueue: .main
)
let cancellable = observer.lastModifiedPublisher
.sink { date in
print("File updated at", date)
}Publishers are well-suited to use with SwiftUI.
- If
appGroupIdis provided but unavailable, storage can fall back to Documents (depending on type/initializer path). - JSON encoding/decoding uses
JSONEncoder/JSONDecoderdefaults.