From boutique
Persist values in Swift apps using Boutique's @StoredValue (UserDefaults) and @SecurelyStoredValue (Keychain). Supports set, reset, toggle, keypath setters, array/dictionary helpers, async observation for preferences, settings, feature flags, auth tokens.
How this skill is triggered — by the user, by Claude, or both
Slash command
/boutique:boutique-stored-valuesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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).
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).
Codable, Sendable, and Equatable.@StoredValue(key: "hasHapticsEnabled")
var hasHapticsEnabled = false
@StoredValue(key: "lastOpenedDate")
var lastOpenedDate: Date? = nil
@StoredValue(key: "currentTheme")
var currentlySelectedTheme: Theme = .light
Always pair with @ObservationIgnored to prevent duplicate observation tracking.
@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
}
Use the $ projected value to access set and reset.
// 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
$hasHapticsEnabled.toggle()
// Equivalent to:
// $hasHapticsEnabled.set(!hasHapticsEnabled)
Update a single property inside a complex stored object without manually copying.
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)
When a @StoredValue holds an array, convenience methods are available.
@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")
@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)
Observe changes over time with the values AsyncStream.
func monitorThemeChanges() async {
for await theme in preferences.$currentlySelectedTheme.values {
print("Theme changed to", theme)
}
}
@StoredValue(key: "sharedSetting", storage: UserDefaults(suiteName: "group.com.example.app")!)
var sharedSetting = false
Useful in contexts where property wrappers are not supported.
let hasHapticsEnabled = StoredValue(key: "hasHapticsEnabled", default: false)
| 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 |
Do not make the type optional yourself, the wrapper handles that. Declaring @SecurelyStoredValue<String?> creates a double optional.
@Observable
final class SecurityManager {
@ObservationIgnored
@SecurelyStoredValue<String>(key: "authToken")
var authToken
@ObservationIgnored
@SecurelyStoredValue<String>(key: "refreshToken")
var refreshToken
}
// 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)
@SecurelyStoredValue<String>(
key: "authToken",
service: KeychainService(value: "com.example.auth"),
group: KeychainGroup(value: "group.com.example.shared")
)
var authToken
@ObservationIgnored
@SecurelyStoredValue<Bool>(key: "biometricsEnabled")
var biometricsEnabled
try $biometricsEnabled.toggle()
@ObservationIgnored
@SecurelyStoredValue<[String]>(key: "trustedDevices")
var trustedDevices
try $trustedDevices.append("device-abc-123")
try $trustedDevices.replace("device-old", with: "device-new")
try $credentials.set(\.accessToken, to: "new-token")
func monitorAuthState() async {
for await token in securityManager.$authToken.values {
if let token {
print("Authenticated")
} else {
print("Logged out")
}
}
}
For apps with many preferences, break them into focused @Observable classes.
@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
}
$: Use $storedValue.set(value), not storedValue.set(value). The wrappedValue is the raw value; the projectedValue (via $) is the StoredValue with mutation methods.@ObservationIgnored: Always add @ObservationIgnored before @StoredValue or @SecurelyStoredValue in @Observable classes.@SecurelyStoredValue<String?>. The wrapper already makes wrappedValue optional.@StoredValue and @SecurelyStoredValue are both @MainActor isolated.@StoredValue are available synchronously on app launch.@SecurelyStoredValue are read from the Keychain synchronously.boutique-swiftui skill for using .binding with SwiftUI controls.npx claudepluginhub mergesort/boutique --plugin boutiqueIntegrate Boutique stores with SwiftUI views using onChange, onStoreDidLoad, bindings, and preview stores for displaying and reacting to persisted data.
Implements SettingsKit for SwiftUI settings interfaces with searchable settings, nested navigation, @Observable/@Bindable state on iOS/macOS/watchOS/tvOS/visionOS. Use for preferences screens or settings errors.
Reviews and implements iOS/macOS Keychain, biometric auth (Face ID/Touch ID), CryptoKit, Secure Enclave, credential storage, certificate pinning, and OWASP mobile compliance.