Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

permissions:
contents: read

jobs:
tests:
name: Lint and test
runs-on: macos-latest
timeout-minutes: 20

steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false

- name: Swift version
run: swift --version

- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 24
cache: yarn
cache-dependency-path: yarn.lock

- name: Install Yarn
run: npm install -g yarn@1.22.22 --ignore-scripts --no-audit --fund=false

- name: Install tooling dependencies
run: yarn install --frozen-lockfile --ignore-scripts --non-interactive

- name: Run CI checks
run: yarn ci:pr-check
46 changes: 46 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Release

on:
push:
branches:
- main
workflow_dispatch:

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

permissions:
contents: write

jobs:
release:
name: Semantic release
runs-on: macos-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 24
cache: yarn
cache-dependency-path: yarn.lock

- name: Install Yarn
run: npm install -g yarn@1.22.22 --ignore-scripts --no-audit --fund=false

- name: Install release dependencies
run: yarn install --frozen-lockfile --ignore-scripts --non-interactive

- name: Run pre-release checks
run: yarn ci:release-check

- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn semantic-release
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ DerivedData
.build/
.swiftpm/
Package.resolved

# Node (semantic-release tooling)
node_modules/
33 changes: 33 additions & 0 deletions .releaserc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module.exports = {
branches: ["main"],
tagFormat: "v${version}",
plugins: [
"./scripts/semantic-release/analyze-commits.cjs",
[
"@semantic-release/release-notes-generator",
{
preset: "conventionalcommits",
},
],
[
"@semantic-release/changelog",
{
changelogFile: "CHANGELOG.md",
},
],
[
"@semantic-release/github",
{
successComment: false,
failComment: false,
},
],
[
"@semantic-release/git",
{
assets: ["CHANGELOG.md"],
message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
},
],
],
};
7 changes: 5 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// swift-tools-version: 5.10
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
name: "SwiftfulCache",
platforms: [
.iOS(.v13)
.iOS(.v18)
],
products: [
.library(
Expand All @@ -23,5 +23,8 @@ let package = Package(
dependencies: ["SwiftfulCache"],
path: "Tests/SwiftfulCacheTests"
),
],
swiftLanguageModes: [
.v6
]
)
140 changes: 74 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,49 @@
# SwiftfulCache

[![Swift 5.10](https://img.shields.io/badge/Swift-5.10-orange.svg)](https://swift.org)
[![Platforms](https://img.shields.io/badge/Platforms-iOS%2013+-blue.svg)](https://developer.apple.com/ios/)
[![Swift 6](https://img.shields.io/badge/Swift-6-orange.svg)](https://swift.org)
[![Platforms](https://img.shields.io/badge/Platforms-iOS%2018+-blue.svg)](https://developer.apple.com/ios/)
[![License](https://img.shields.io/badge/License-MIT%20%2B%20AI%20Restriction-lightgrey.svg)](LICENSE)

A lightweight Swift wrapper around `NSCache` that provides both volatile (in-memory) and persistent (disk-backed) caching with expiration support.
SwiftfulCache is a lightweight wrapper around `NSCache` that adds expiration and optional disk persistence for `Codable` types.

## Features

- **Volatile cache** — in-memory caching backed by `NSCache` with automatic eviction and configurable limits
- **Persistent cache** — JSON-based disk persistence for `Codable` types
- **Expiration** — configurable cache lifetime with automatic cleanup of stale entries
- **Subscript access** — read and write cache entries with `cache[key]` syntax
- **Pure Swift** — no external dependencies
- In-memory caching backed by `NSCache`
- Configurable cache lifetime (expiration)
- Configurable maximum number of cached values
- Optional JSON persistence for `Codable` key/value pairs
- Easy API with `set`, `get`, `remove`, `getKeys`, and subscript support
- No external dependencies

## Requirements

- Swift 6+
- iOS 18.0+

## Installation

### Swift Package Manager

Add SwiftfulCache to your project via Xcode:
Add SwiftfulCache in Xcode:

1. Go to **File > Add Package Dependencies...**
2. Enter the repository URL:
2. Use the repository URL:

```
https://github.com/vigotskij/SwiftfulCache.git
```
3. Select your desired version rule and add the package.

Or add it directly to your `Package.swift`:
3. Select your preferred version rule and add the package.

Or add it to your `Package.swift`:

```swift
dependencies: [
.package(url: "https://github.com/vigotskij/SwiftfulCache.git", from: "0.1.0")
]
```

Then add `"SwiftfulCache"` as a dependency of the target that will use it:
Then add `"SwiftfulCache"` to your target dependencies:

```swift
.target(
Expand All @@ -44,9 +52,7 @@ Then add `"SwiftfulCache"` as a dependency of the target that will use it:
)
```

## Usage

### Volatile Cache (in-memory)
## Quick Start

```swift
import SwiftfulCache
Expand All @@ -56,79 +62,81 @@ let cache = Cache<String, String>(
maximumCachedValues: 100
)

// Store a value
cache.setValue("Hello", forKey: "greeting")

// Retrieve a value
let value = cache.getValue(forKey: "greeting") as? String

// Subscript access
cache["greeting"] = "Hi"
let greeting = cache["greeting"] as? String

// Clear all in-memory entries
cache.removeValue(forKey: "greeting")
cache.clearVolatile()
```

### Persistent Cache (disk-backed)
## Persistence

When `Key` and `Value` conform to `Codable`, you get persistence for free:
If `Key` and `Value` conform to `Codable`, the same cache can persist data to disk and load it back:

```swift
let cache = Cache<String, MyCodableModel>()

cache.setValue(model, forKey: "user_profile")

// Persist to disk
cache.persist(withName: "user_cache", using: .default)
let persistResult = cache.persist(withName: "user_cache", using: .default)
let loadResult = cache.load(withName: "user_cache", using: .default)
let clearResult = cache.clearPersistence(withName: "user_cache", using: .default)
```

Persistence uses the system caches directory by default.

## Protocol-based Usage

// Load from disk
cache.load(withName: "user_cache", using: .default)
You can work against the protocols when you want to abstract behavior:

// Remove persisted file
cache.clearPersistence(withName: "user_cache", using: .default)
```swift
let volatileCache: VolatileCacheable = Cache<String, YourObject>()
let persistentCache: PersistentCacheable = Cache<String, YourCodableObject>()
```

## Requirements
## CI and Releases

- iOS 13.0+
- Swift 5.10+
This repository uses GitHub Actions for automation:

## Usage
After that, you can use it importing the framework and initializing a specialized Cache, for example:
```swift
class SomeClass {
// You can use only the RAM Cache
let volatileCache: VolatileCacheable = Cache<String, YourObject>()
// or Persistent which includes Volatile
let persistentCache: PersistentCacheable = Cache<String, YourObject>()
// or, just the Cache class, which has a few default values on the function signatures.
// Using the Cache class will load both Volatile and Persistent Cache
let cache: Cache<String, YourObject> = .init()
}
```

For persistent cache, using the `.cachesDirectory` allows the system to clean the files as its own rules. A valid alternative is to use `.documentsDirectory` which will allow you to store the data, as long as your app exists, by your own terms.

## What is done
* Volatile cache set, get, remove, get keys, and subscript functions
* Persistent cache persist to file and load from file to Volatile cache functions
* Volatile tests for set, get, remove, get keys functions
* Volatile performance tests for set, get, remove and get keys functions
* Persistent parcial tests for both persist and load functions
* Added basic error handling for persist and load functions
* PersistentCacheable full test coverage for persist and load functions
* PersistentCacheable performance tests

# References
This was created inspired by the [work](https://www.swiftbysundell.com/articles/caching-in-swift/) of [John Sundell](https://github.com/JohnSundell).

And also using Apple's related documentation:
- [NSCache](https://developer.apple.com/documentation/foundation/nscache)
- [FileManager](https://developer.apple.com/documentation/foundation/filemanager)## Author
- On each PR, CI runs the shared checks:
- `yarn lane:lint` (strict concurrency + warnings-as-errors)
- `yarn lane:test`
- On push to `main`, the release workflow runs the same checks and then runs `semantic-release` (executed with Yarn).

These shared commands are intentionally "lane-like" so they can be reused later from Fastlane lanes.

Security hardening applied in CI workflows:

- Least-privilege GitHub token permissions on PR workflows
- JavaScript actions pinned to immutable commit SHAs
- Read-only checkout credentials on PR workflows
- Dependency installs with script execution disabled

Release behavior from commit titles:

- `feat:` -> minor release
- `fix:`, `perf:`, `chore:` -> patch release
- `BREAKING CHANGE` or `!` after type/scope -> major release
- Add `(no-release)` anywhere in the commit message to skip release for that commit

Examples:

- `chore: update docs` -> patch release
- `chore: update docs (no-release)` -> no release

## References

- Inspired by [Caching in Swift by John Sundell](https://www.swiftbysundell.com/articles/caching-in-swift/)
- Apple documentation:
- [NSCache](https://developer.apple.com/documentation/foundation/nscache)
- [FileManager](https://developer.apple.com/documentation/foundation/filemanager)

## Author

[Boris Sortino](https://linkedin.com/in/bsortino/)

## License

SwiftfulCache is available under the MIT license with an additional restriction prohibiting use for AI/ML training. See the [LICENSE](LICENSE) file for full details.
SwiftfulCache is available under the MIT license with an additional restriction prohibiting use for AI/ML training. See [LICENSE](LICENSE) for full details.
21 changes: 21 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "swiftfulcache-release",
"private": true,
"packageManager": "yarn@1.22.22",
"scripts": {
"lane:lint": "swift build -Xswiftc -strict-concurrency=complete -Xswiftc -warnings-as-errors",
"lane:test": "swift test",
"ci:pr-check": "yarn lane:lint && yarn lane:test",
"ci:release-check": "yarn ci:pr-check",
"semantic-release": "semantic-release",
"release": "yarn semantic-release"
},
"devDependencies": {
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.0",
"@semantic-release/release-notes-generator": "^14.1.0",
"conventional-changelog-conventionalcommits": "^8.0.0",
"semantic-release": "^24.2.7"
}
}
Loading