Skip to content

kvaDrug/FileStorage

Repository files navigation

FileStorage

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.

Features

  • FileObjectStorage<Content> for a single Codable value
  • FileArrayStorage<Element> for append/trim/sort array workflows
  • FileDictionaryStorage<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
  • FileChangeObserver for file-change notifications via NSFilePresenter

Requirements

  • Swift tools: 6.2
  • iOS 16.0+
  • watchOS 9.0+
  • macOS 13.0+

Installation (Swift Package Manager)

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")
            ]
        )
    ]
)

Quick Start

import FileStorage

Store a single object

struct 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()

Store an array

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()

Store a dictionary

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")

Coordinated Access

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)
}

File Change Observation

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.

Notes

  • If appGroupId is provided but unavailable, storage can fall back to Documents (depending on type/initializer path).
  • JSON encoding/decoding uses JSONEncoder/JSONDecoder defaults.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors