Skip to content
Back to skills

Boutique Stored Values

ASecurity

Persist individual values with Boutique's @StoredValue (UserDefaults) and @SecurelyStoredValue (Keychain), including set, reset, toggle, bindings, keypath setters, array and dictionary helpers, and async observation. Use when storing preferences, settings, feature flags, or sensitive data like auth tokens.

  • 4 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 12, 2026
securityrustswiftsecurity

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 12, 2026

npx -y skills add aibot88/sec_skill_store --skill boutique-stored-values --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Boutique Stored Values?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Boutique Stored Values
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/aibot88-boutique-stored-values/badge)](https://www.skillsdirectory.com/skills/aibot88-boutique-stored-values)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: boutique-stored-values
description: Persist individual values with Boutique's @StoredValue (UserDefaults) and @SecurelyStoredValue (Keychain), including set, reset, toggle, bindings, keypath setters, array and dictionary helpers, and async observation. Use when storing preferences, settings, feature flags, or sensitive data like auth tokens.
---

# Boutique Stored Values

Use this skill when you need to persist individual values (preferences, settings, feature flags) using `@StoredValue` (backed by UserDefaults) or sensitive data (auth tokens, passwords) using `@SecurelyStoredValue` (backed by the system Keychain).

## Prerequisites

- Boutique added as a dependency via Swift Package Manager.
- Stored values must conform to `Codable`, `Sendable`, and `Equatable`.

## @StoredValue (UserDefaults)

### Basic declaration

```swift
@StoredValue(key: "hasHapticsEnabled")
var hasHapticsEnabled = false

@StoredValue(key: "lastOpenedDate")
var lastOpenedDate: Date? = nil

@StoredValue(key: "currentTheme")
var currentlySelectedTheme: Theme = .light
```

### In @Observable classes

Always pair with `@ObservationIgnored` to prevent duplicate observation tracking.

```swift
@Observable
final class Preferences {
    @ObservationIgnored
    @StoredValue(key: "hasHapticsEnabled")
    var hasHapticsEnabled = false

    @ObservationIgnored
    @StoredValue(key: "lastOpenedDate")
    var lastOpenedDate: Date? = nil

    @ObservationIgnored
    @StoredValue(key: "currentTheme")
    var currentlySelectedTheme: Theme = .light
}
```

### Setting and resetting values

Use the `$` projected value to access `set` and `reset`.

```swift
// Set a new value
$lastOpenedDate.set(.now)
$currentlySelectedTheme.set(.dark)

// Reset to the default value provided at declaration
$lastOpenedDate.reset()          // Back to nil
$currentlySelectedTheme.reset()  // Back to .light
```

### Boolean toggle

```swift
$hasHapticsEnabled.toggle()

// Equivalent to:
// $hasHapticsEnabled.set(!hasHapticsEnabled)
```

### Keypath setter (nested property updates)

Update a single property inside a complex stored object without manually copying.

```swift
struct UserPreferences: Codable, Sendable, Equatable {
    var hasHapticsEnabled: Bool
    var prefersDarkMode: Bool
    var prefersWideScreen: Bool
}

@ObservationIgnored
@StoredValue(key: "userPreferences")
var preferences = UserPreferences(
    hasHapticsEnabled: true,
    prefersDarkMode: false,
    prefersWideScreen: false
)

// Update a single nested property
$preferences.set(\.prefersDarkMode, to: true)
```

### Array helpers

When a `@StoredValue` holds an array, convenience methods are available.

```swift
@ObservationIgnored
@StoredValue(key: "favoriteTags")
var favoriteTags: [String] = []

// Append an element
$favoriteTags.append("swift")

// Toggle presence (add if missing, remove if present)
$favoriteTags.togglePresence("swift")

// Replace an element
$favoriteTags.replace("swfit", with: "swift")
```

### Dictionary helpers

```swift
@ObservationIgnored
@StoredValue(key: "featureFlags")
var featureFlags: [String: Bool] = [:]

// Update a key
$featureFlags.update(key: "darkMode", value: true)

// Remove a key by setting nil
$featureFlags.update(key: "darkMode", value: nil)
```

### Async observation

Observe changes over time with the `values` AsyncStream.

```swift
func monitorThemeChanges() async {
    for await theme in preferences.$currentlySelectedTheme.values {
        print("Theme changed to", theme)
    }
}
```

### Custom UserDefaults

```swift
@StoredValue(key: "sharedSetting", storage: UserDefaults(suiteName: "group.com.example.app")!)
var sharedSetting = false
```

### Direct initialization (without property wrapper syntax)

Useful in contexts where property wrappers are not supported.

```swift
let hasHapticsEnabled = StoredValue(key: "hasHapticsEnabled", default: false)
```

## @SecurelyStoredValue (Keychain)

### Key differences from @StoredValue

| Aspect                | @StoredValue          | @SecurelyStoredValue        |
|-----------------------|-----------------------|-----------------------------|
| Backing store         | UserDefaults          | System Keychain             |
| Default value         | Required              | Not supported               |
| `wrappedValue` type   | `Item`                | `Item?` (always optional)   |
| Mutation methods      | `set(_:)`, `reset()`  | `set(_:) throws`, `remove() throws` |
| Use case              | Preferences, settings | Passwords, tokens, secrets  |

### Declaration

Do not make the type optional yourself, the wrapper handles that. Declaring `@SecurelyStoredValue<String?>` creates a double optional.

```swift
@Observable
final class SecurityManager {
    @ObservationIgnored
    @SecurelyStoredValue<String>(key: "authToken")
    var authToken

    @ObservationIgnored
    @SecurelyStoredValue<String>(key: "refreshToken")
    var refreshToken
}
```

### Setting and removing values

```swift
// Set a value (throws on keychain errors)
try $authToken.set("eyJhbGciOiJIUzI1NiIs...")

// Remove from keychain
try $authToken.remove()

// Set to nil (same as remove)
try $authToken.set(nil)
```

### Keychain service and group

```swift
@SecurelyStoredValue<String>(
    key: "authToken",
    service: KeychainService(value: "com.example.auth"),
    group: KeychainGroup(value: "group.com.example.shared")
)
var authToken
```

### Boolean toggle (throws)

```swift
@ObservationIgnored
@SecurelyStoredValue<Bool>(key: "biometricsEnabled")
var biometricsEnabled

try $biometricsEnabled.toggle()
```

### Array and dictionary helpers (throws)

```swift
@ObservationIgnored
@SecurelyStoredValue<[String]>(key: "trustedDevices")
var trustedDevices

try $trustedDevices.append("device-abc-123")
try $trustedDevices.replace("device-old", with: "device-new")
```

### Keypath setter (throws)

```swift
try $credentials.set(\.accessToken, to: "new-token")
```

### Async observation

```swift
func monitorAuthState() async {
    for await token in securityManager.$authToken.values {
        if let token {
            print("Authenticated")
        } else {
            print("Logged out")
        }
    }
}
```

## Structuring Preferences

For apps with many preferences, break them into focused `@Observable` classes.

```swift
@Observable
final class Preferences {
    var userExperience = UserExperiencePreferences()
    var notifications = NotificationPreferences()
}

@Observable
final class UserExperiencePreferences {
    @ObservationIgnored
    @StoredValue(key: "hasSoundEffectsEnabled")
    var hasSoundEffectsEnabled = false

    @ObservationIgnored
    @StoredValue(key: "hasHapticsEnabled")
    var hasHapticsEnabled = true
}

@Observable
final class NotificationPreferences {
    @ObservationIgnored
    @StoredValue(key: "pushEnabled")
    var pushEnabled = true

    @ObservationIgnored
    @StoredValue(key: "emailDigestEnabled")
    var emailDigestEnabled = false
}
```

## Common Mistakes

- **Forgetting `$`**: Use `$storedValue.set(value)`, not `storedValue.set(value)`. The `wrappedValue` is the raw value; the `projectedValue` (via `$`) is the `StoredValue` with mutation methods.
- **Missing `@ObservationIgnored`**: Always add `@ObservationIgnored` before `@StoredValue` or `@SecurelyStoredValue` in `@Observable` classes.
- **Double optional**: Don't write `@SecurelyStoredValue<String?>`. The wrapper already makes `wrappedValue` optional.

## Notes

- `@StoredValue` and `@SecurelyStoredValue` are both `@MainActor` isolated.
- Values from `@StoredValue` are available synchronously on app launch.
- Values from `@SecurelyStoredValue` are read from the Keychain synchronously.
- See `boutique-swiftui` skill for using `.binding` with SwiftUI controls.

Files in this skill

  • SKILL.md7.6 KB
  • source.json848 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.