dankinsoid/swift-configs
A unified Swift API for configuration management that supports multiple backends and provides type-safe access to configuration values.
Features
- Unified API for Small Key-Value Stores: Works with UserDefaults, Keychain, environment variables, in-memory storage, and other enumerable key-value systems
- Configuration Categories: High-level abstraction that allows changing storage backends without modifying code that uses the values
- Type Safety: Full support for any
Codablevalues out of the box with compile-time type checking - Flexible Key Configuration: Individual keys can use specific stores instead of abstract categories, allowing usage before system bootstrap
- Easy Storage Migration: Seamlessly migrate between different storage backends or individual key migrations
- Test and Preview Support: Automatically uses in-memory storage for SwiftUI previews and can be easily configured for testing
- Per-Key Customization: Each configuration key can have its own store, transformer, or migration logic
- Property Wrapper APIs for simpler usage
- Real-time Updates with cancellable change subscriptions
- Secure Storage Options including Keychain and Secure Enclave support
Getting Started
1. Import SwiftConfigs
import SwiftConfigs2. Define Configuration Keys
public extension Configs.Keys {
var apiToken: RWConfigKey<String?> {
ConfigKey("api-token", in: .secure)
}
var userID: ROConfigKey<UUID> {
ConfigKey("USER_ID", in: .syncedSecure, default: UUID(), cacheDefaultValue: true)
}
var serverURL: ROConfigKey<String> {
ConfigKey("SERVER_URL", in: .environment, default: "https://api.example.com")
}
}3. Create a Configs Instance
let configs = Configs()4. Use Your Configuration
// Read values
let userID = configs.userID
let token = configs.apiToken
let serverURL = configs.serverURL
// Write values (for RWConfigKey only)
configs.apiToken = "new-token"Configuration Categories
SwiftConfigs organizes configuration data using categories, allowing you to store different types of settings in appropriate backends:
ConfigSystem.bootstrap([
.default: .userDefaults, // General app settings
.secure: .keychain, // Sensitive data (tokens, passwords)
.critical: .secureEnclave(), // Maximum security with biometrics
.syncedSecure: .keychain(iCloudSync: true), // Synced secure data
.environment: .environment, // Environment variables
.memory: .inMemory, // Temporary/testing data
.remote: .userDefaults, // Remote configuration cache
.manifest: .infoPlist // App Info.plist values
])Built-in Categories
.default- General application settings.synced- Data synced across devices.secure- Sensitive data requiring encryption.critical- Maximum security with hardware protection.syncedSecure- Secure data synced across devices.environment- Environment variables.memory- In-memory storage.remote- Remote configuration cache.manifest- App manifest values, e.g. Info.plist
Available Stores
UserDefaults
.userDefaults // Standard UserDefaults
.userDefaults(suiteName: "group") // App group UserDefaultsKeychain (iOS/macOS)
.keychain // Basic keychain storage
.keychain(iCloudSync: true) // iCloud Keychain sync
.secureEnclave() // Secure Enclave with user presence
.biometricSecureEnclave() // Secure Enclave with biometrics
.passcodeSecureEnclave() // Secure Enclave with device passcodeiCloud Key-Value Store
.ubiquitous // Default iCloud key-value store
.ubiquitous(store: customUbiquitousStore) // Custom iCloud store instanceOther Stores
.environment // Environment variables (read-only)
.infoPlist // App bundle Info.plist (read-only)
.infoPlist(for: bundle) // Custom bundle Info.plist
.inMemory // In-memory storage
.inMemory(["key": "value"]) // In-memory with initial values
.multiple(store1, store2) // Multiplex multiple stores (fallback chain)Property Wrapper API
Use property wrappers for inline configuration management:
struct AppSettings {
// Using key path reference to predefined keys
@ROConfig(\.userID)
var userID: UUID
// Using category-based initialization (recommended)
@RWConfig("api-token", in: .secure)
var apiToken: String?
@RWConfig("user-preferences", in: .default)
var preferences = UserPreferences()
// Using store-based initialization (for specific store targeting)
@RWConfig("debug-mode", store: .inMemory)
var debugMode = false
}
let settings = AppSettings()
print(settings.userID) // Read value
settings.apiToken = "new-token" // Write value
settings.preferences.theme = .darkSwiftUI Property Wrappers
For SwiftUI views, use ROConfigState and RWConfigState property wrappers that automatically trigger view updates when configuration changes:
struct SettingsView: View {
// Read-only configuration with automatic view updates
@ROConfigState(\.userID)
var userID: UUID
// Read-write configuration with automatic view updates
@RWConfigState("theme", in: .default)
var theme = Theme.light
@RWConfigState("counter", in: .default)
var counter = 0
var body: some View {
VStack {
Text("User: \(userID)")
Picker("Theme", selection: $theme) {
Text("Light").tag(Theme.light)
Text("Dark").tag(Theme.dark)
}
Text("Count: \(counter)")
Button("Increment") {
counter += 1
}
}
}
}Namespaces
SwiftConfigs supports namespace-based organization of configuration keys, providing compile-time structure and type safety for logically related keys.
Basic Namespaces
Group related keys in namespace extensions of Configs.Keys:
public extension Configs.Keys {
var security: Security { Security() }
struct Security: ConfigNamespaceKeys {}
}
extension Configs.Keys.Security {
public var apiToken: RWConfigKey<String?> {
ConfigKey("api-token", in: .secure)
}
public var encryptionEnabled: ROConfigKey<Bool> {
ConfigKey("encryption-enabled", in: .secure, default: true)
}
}
// Usage - clean, organized access
let configs = Configs()
let apiToken = configs.security.apiToken
configs.security.encryptionEnabled = false
// Property wrapper usage
@RWConfig(\.security.apiToken) var token: String?
@ROConfigState(\.security.encryptionEnabled) var isEncryptionEnabled: BoolNested Namespaces
Create deeper hierarchies by nesting namespace types:
public extension Configs.Keys {
var features: Features { Features() }
struct Features: ConfigNamespaceKeys {
public var auth: Auth { Auth() }
public struct Auth: ConfigNamespaceKeys {}
}
}
extension Configs.Keys.Features.Auth {
public var biometricEnabled: RWConfigKey<Bool> {
ConfigKey("biometric-enabled", in: .default, default: false)
}
}
// Usage - deep namespace navigation
let biometricEnabled = configs.features.auth.biometricEnabled
configs.features.auth.biometricEnabled = trueKey Prefixing (Optional)
Namespaces are primarily for code organization. But if needed, you can add a keyPrefix to automatically prefix all keys in that namespace:
public extension Configs.Keys {
var environment: Environment { Environment() }
struct Environment: ConfigNamespaceKeys {
public var keyPrefix: String { "env/" } // Optional key prefixing
public var apiUrl: ROConfigKey<String> {
ConfigKey(qualify("api-url"), in: .environment, default: "localhost") // "env/api-url"
}
}
}Async/Await Support
let configs = Configs()
// Fetch latest values
try await configs.fetch()
// Fetch and get specific value
let token = try await configs.fetch(configs.apiToken)
// Fetch only if needed
let value = try await configs.fetchIfNeeded(configs.someKey)Listening for Changes
Callback-based Listening
let configs = Configs()
// Listen to all configuration changes
let cancellation = configs.onChange { configs in
print("Configurations updated")
}
// Listen to specific key changes
let keyCancellation = configs.onChange(\.apiToken) { newToken in
print("API token changed: \(newToken)")
}
// Cancel when done
cancellation.cancel()
keyCancellation.cancel()Async Sequence-based Listening
let configs = Configs()
// Listen to all configuration changes using async sequences
for await updatedConfigs in configs.changes() {
print("Configurations updated")
}
// Listen to specific key changes using async sequences
for await newToken in configs.changes(for: \.apiToken) {
print("API token changed: \(newToken)")
}
// Use in async context with cancellation
let task = Task {
for await newToken in configs.changes(for: \.apiToken) {
print("API token changed: \(newToken)")
// Break on specific condition
if newToken == "expected-token" {
break
}
}
}
// Cancel the task when needed
task.cancel()Combine Publisher Support
When Combine is available, configuration changes can also be used as Publishers:
import Combine
let configs = Configs()
var cancellables = Set<AnyCancellable>()
// Listen to configuration changes using Combine
configs.changes()
.sink { updatedConfigs in
print("Configurations updated")
}
.store(in: &cancellables)
// Listen to specific key changes using Combine
configs.changes(for: \.apiToken)
.sink { newToken in
print("API token changed: \(newToken)")
}
.store(in: &cancellables)
// Chain with other Combine operators
configs.changes(for: \.apiToken)
.compactMap { $0 }
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { debouncedToken in
print("Debounced API token: \(debouncedToken)")
}
.store(in: &cancellables)Value Transformers
SwiftConfigs automatically handles common types:
public extension Configs.Keys {
// String-convertible types
var count: ROConfigKey<Int> {
ConfigKey("count", in: .default, default: 0)
}
var rate: ROConfigKey<Double> {
ConfigKey("rate", in: .default, default: 1.0)
}
// Enum types
var theme: ROConfigKey<Theme> {
ConfigKey("theme", in: .default, default: .light)
}
// Codable types (stored as JSON)
var settings: ROConfigKey<AppSettings> {
ConfigKey("settings", in: .default, default: AppSettings())
}
// Optional types
var optionalValue: ROConfigKey<String?> {
ConfigKey("optional", in: .default)
}
// Using specific stores when needed
var tempSetting: RWConfigKey<String> {
ConfigKey("temp", store: .inMemory, default: "temp-value")
}
var secureToken: RWConfigKey<String?> {
ConfigKey("secure-token", store: .keychain)
}
}Configuration Migration
Handle configuration schema changes gracefully:
public extension Configs.Keys {
// Migrate from old boolean to new enum
var notificationStyle: ROConfigKey<NotificationStyle> {
ConfigKey("notification-style", in: .default, default: .none)
}
private var oldNotificationsEnabled: ROConfigKey<Bool> {
ConfigKey("notifications-enabled", in: .default, default: false)
}
// Custom migration using multiplex stores can be done at bootstrap level:
// ConfigSystem.bootstrap([
// .default: .multiple(.userDefaults, .inMemory) // Check multiple sources
// ])
}Custom Configuration Stores
Create custom storage backends by implementing the ConfigStore protocol:
import Foundation
struct MyCustomStore: ConfigStore {
var isWritable: Bool { true }
func fetch(completion: @escaping (Error?) -> Void) {
// Fetch latest values from your backend
completion(nil)
}
func onChange(_ listener: @escaping () -> Void) -> Cancellation {
// Set up change notifications
return Cancellation { /* cleanup */ }
}
func onChangeOfKey(_ key: String, _ listener: @escaping (String?) -> Void) -> Cancellation {
// Set up key-specific change notifications
return Cancellation { /* cleanup */ }
}
func get(_ key: String) throws -> String? {
// Retrieve value for key
return myDatabase.getValue(key)
}
func set(_ value: String?, for key: String) throws {
// Store value for key
if let value = value {
myDatabase.setValue(value, forKey: key)
} else {
myDatabase.removeValue(forKey: key)
}
}
func exists(_ key: String) throws -> Bool {
return myDatabase.hasValue(forKey: key)
}
func removeAll() throws {
myDatabase.clearAll()
}
func keys() -> Set<String>? {
return Set(myDatabase.allKeys())
}
}
// Use your custom store
ConfigSystem.bootstrap([
.default: MyCustomStore(),
.secure: .keychain
])Available Implementations
There is a ready-to-use ConfigStore implementation:
Firebase Remote Config
- Repository: swift-firebase-tools
- Features: Remote configuration management, A/B testing, real-time updates
- Use case: Server-controlled feature flags and configuration values
// Add to Package.swift
.package(url: "https://github.com/dankinsoid/swift-firebase-tools.git", from: "0.3.0")
// Usage
import FirebaseConfigs
ConfigSystem.bootstrap([
.default: .userDefaults,
.remote: .firebaseRemoteConfig
])Community Contributions
Want to add your own ConfigStore implementation? Consider contributing to the ecosystem by:
- Creating a separate package with your store
- Following the
ConfigStoreprotocol - Adding comprehensive tests and documentation
- Submitting your package for inclusion in this list
Installation
Swift Package Manager
Add SwiftConfigs to your Package.swift:
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "YourProject",
dependencies: [
.package(url: "https://github.com/dankinsoid/swift-configs.git", from: "1.0.0")
],
targets: [
.target(name: "YourProject", dependencies: ["SwiftConfigs"])
]
)Or add it through Xcode:
- Go to File → Add Package Dependencies
- Enter:
https://github.com/dankinsoid/swift-configs.git - Choose the version and add to your target
Best Practices
- Define keys as computed properties in
Configs.Keysextensions for organization and discoverability - Use namespaces for organization - group related keys into
ConfigNamespaceKeystypes for compile-time structure - Use appropriate categories for different security and persistence needs
- Provide sensible defaults for all configuration keys
- Use read-only keys (
ROConfigKey) when values shouldn't be modified at runtime - Bootstrap the system early in your app lifecycle before accessing any configuration
- Prefer category-based initialization (
init(_:in:default:)) over store-based for most use cases - Use store-based initialization (
init(_:store:default:)) only when you need specific store targeting or before system bootstrap - Use prefixing sparingly - only add
keyPrefixwhen you need it; most namespaces work fine with the default empty prefix - Handle migration using multiplex stores or custom migration logic
- Use property wrappers for clean SwiftUI and declarative code integration
- Leverage async/await for remote configuration fetching
- Use change observation for reactive configuration updates
Security Considerations
- Use
.securecategory for sensitive data (API tokens, passwords) - uses Keychain encryption - Use
.criticalfor maximum security with hardware-backed Secure Enclave protection - Use
.syncedSecurecarefully - only for data that should be shared across devices via iCloud Keychain - Never log configuration values that might contain sensitive data
- Environment variables are read-only and visible to the entire process and system
- Keychain accessibility levels control when encrypted data can be accessed (device locked/unlocked)
- Biometric authentication adds an extra layer of security for critical configuration data
- iCloud sync (
.ubiquitous) has a 1MB total storage limit and is eventually consistent
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
SwiftConfigs is available under the MIT license. See the LICENSE file for more info.
Package Metadata
Repository: dankinsoid/swift-configs
Stars: 8
Forks: 0
Open issues: 0
Default branch: main
Primary language: swift
License: MIT
README: README.md