{"id":30719823,"url":"https://github.com/dankinsoid/swift-configs","last_synced_at":"2025-09-03T10:42:19.279Z","repository":{"id":307194828,"uuid":"1028674865","full_name":"dankinsoid/swift-configs","owner":"dankinsoid","description":"A unified Swift API for configuration management that supports multiple backends and provides type-safe access to configuration values.","archived":false,"fork":false,"pushed_at":"2025-08-30T09:35:04.000Z","size":226,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-30T11:22:31.033Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dankinsoid.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-29T22:26:42.000Z","updated_at":"2025-08-30T09:35:07.000Z","dependencies_parsed_at":"2025-08-22T21:07:30.434Z","dependency_job_id":"3329db7e-fa6f-4bf6-be2b-37c18c3c20c7","html_url":"https://github.com/dankinsoid/swift-configs","commit_stats":null,"previous_names":["dankinsoid/swift-configs"],"tags_count":41,"template":false,"template_full_name":null,"purl":"pkg:github/dankinsoid/swift-configs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2Fswift-configs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2Fswift-configs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2Fswift-configs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2Fswift-configs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dankinsoid","download_url":"https://codeload.github.com/dankinsoid/swift-configs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2Fswift-configs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273431361,"owners_count":25104491,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-03T02:00:09.631Z","response_time":76,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-09-03T10:42:18.315Z","updated_at":"2025-09-03T10:42:19.263Z","avatar_url":"https://github.com/dankinsoid.png","language":"Swift","funding_links":[],"categories":["Swift"],"sub_categories":[],"readme":"# SwiftConfigs\n\nSwiftConfigs provides a unified, type-safe API for small key-value storage systems where keys can be manually enumerated. The library abstracts storage implementation details behind a clean interface, making it easy to switch between different backends without changing application code.\n\n## Features\n\n- **Unified API for Small Key-Value Stores**: Works with UserDefaults, Keychain, environment variables, in-memory storage, and other enumerable key-value systems\n- **Configuration Categories**: High-level abstraction that allows changing storage backends without modifying code that uses the values\n- **Type Safety**: Full support for any `Codable` values out of the box with compile-time type checking\n- **Flexible Key Configuration**: Individual keys can use specific stores instead of abstract categories, allowing usage before system bootstrap\n- **Easy Storage Migration**: Seamlessly migrate between different storage backends or individual key migrations\n- **Test and Preview Support**: Automatically uses in-memory storage for SwiftUI previews and can be easily configured for testing\n- **Per-Key Customization**: Each configuration key can have its own store, transformer, or migration logic\n- **Property Wrapper APIs** for simpler usage\n- **Real-time Updates** with cancellable change subscriptions\n- **Secure Storage Options** including Keychain and Secure Enclave support\n\n## Getting Started\n\n### 1. Import SwiftConfigs\n\n```swift\nimport SwiftConfigs\n```\n\n### 2. Define Configuration Keys\n\n```swift\npublic extension Configs.Keys {\n    \n    var apiToken: RWConfigKey\u003cString?\u003e {\n        ConfigKey(\"api-token\", in: .secure)\n    }\n    \n    var userID: ROConfigKey\u003cUUID\u003e { \n        ConfigKey(\"USER_ID\", in: .syncedSecure, default: UUID(), cacheDefaultValue: true)\n    }\n    \n    var serverURL: ROConfigKey\u003cString\u003e { \n        ConfigKey(\"SERVER_URL\", in: .environment, default: \"https://api.example.com\")\n    }\n}\n```\n\n### 3. Create a Configs Instance\n\n```swift\nlet configs = Configs()\n```\n\n### 4. Use Your Configuration\n\n```swift\n// Read values\nlet userID = configs.userID\nlet token = configs.apiToken\nlet serverURL = configs.serverURL\n\n// Write values (for RWConfigKey only)\nconfigs.apiToken = \"new-token\"\n```\n\n## Configuration Categories\n\nSwiftConfigs organizes configuration data using categories, allowing you to store different types of settings in appropriate backends:\n\n```swift\nConfigSystem.bootstrap([\n    .default: .userDefaults,           // General app settings\n    .secure: .keychain,                // Sensitive data (tokens, passwords)\n    .critical: .secureEnclave(),       // Maximum security with biometrics\n    .syncedSecure: .keychain(iCloudSync: true), // Synced secure data\n    .environment: .environment,        // Environment variables\n    .memory: .inMemory,                // Temporary/testing data\n    .remote: .userDefaults,            // Remote configuration cache\n    .manifest: .infoPlist              // App Info.plist values\n])\n```\n\n### Built-in Categories\n\n- **`.default`** - General application settings\n- **`.synced`** - Data synced across devices\n- **`.secure`** - Sensitive data requiring encryption\n- **`.critical`** - Maximum security with hardware protection\n- **`.syncedSecure`** - Secure data synced across devices\n- **`.environment`** - Environment variables\n- **`.memory`** - In-memory storage\n- **`.remote`** - Remote configuration cache\n- **`.manifest`** - App manifest values, e.g. Info.plist\n\n## Available Stores\n\n### UserDefaults\n```swift\n.userDefaults                     // Standard UserDefaults\n.userDefaults(suiteName: \"group\") // App group UserDefaults\n```\n\n### Keychain (iOS/macOS)\n```swift\n.keychain                                 // Basic keychain storage\n.keychain(iCloudSync: true)               // iCloud Keychain sync\n.secureEnclave()                          // Secure Enclave with user presence\n.biometricSecureEnclave()                 // Secure Enclave with biometrics\n.passcodeSecureEnclave()                  // Secure Enclave with device passcode\n```\n\n### iCloud Key-Value Store\n```swift\n.ubiquitous                               // Default iCloud key-value store\n.ubiquitous(store: customUbiquitousStore) // Custom iCloud store instance\n```\n\n### Other Stores\n```swift\n.environment                              // Environment variables (read-only)\n.infoPlist                                // App bundle Info.plist (read-only)\n.infoPlist(for: bundle)                   // Custom bundle Info.plist\n.inMemory                                 // In-memory storage\n.inMemory([\"key\": \"value\"])               // In-memory with initial values\n.multiple(store1, store2)                 // Multiplex multiple stores (fallback chain)\n```\n\n## Property Wrapper API\n\nUse property wrappers for inline configuration management:\n\n```swift\nstruct AppSettings {\n    \n    // Using key path reference to predefined keys\n    @ROConfig(\\.userID) \n    var userID: UUID\n    \n    // Using category-based initialization (recommended)\n    @RWConfig(\"api-token\", in: .secure) \n    var apiToken: String?\n    \n    @RWConfig(\"user-preferences\", in: .default)\n    var preferences = UserPreferences()\n    \n    // Using store-based initialization (for specific store targeting)\n    @RWConfig(\"debug-mode\", store: .inMemory) \n    var debugMode = false\n}\n\nlet settings = AppSettings()\nprint(settings.userID)           // Read value\nsettings.apiToken = \"new-token\"  // Write value\nsettings.preferences.theme = .dark\n```\n\n## SwiftUI Property Wrappers\n\nFor SwiftUI views, use `ROConfigState` and `RWConfigState` property wrappers that automatically trigger view updates when configuration changes:\n\n```swift\nstruct SettingsView: View {\n    \n    // Read-only configuration with automatic view updates\n    @ROConfigState(\\.userID) \n    var userID: UUID\n    \n    // Read-write configuration with automatic view updates\n    @RWConfigState(\"theme\", in: .default) \n    var theme = Theme.light\n    \n    @RWConfigState(\"counter\", in: .default) \n    var counter = 0\n    \n    var body: some View {\n        VStack {\n            Text(\"User: \\(userID)\")\n            \n            Picker(\"Theme\", selection: $theme) {\n                Text(\"Light\").tag(Theme.light)\n                Text(\"Dark\").tag(Theme.dark)\n            }\n            \n            Text(\"Count: \\(counter)\")\n            \n            Button(\"Increment\") {\n                counter += 1\n            }\n        }\n    }\n}\n```\n\n## Namespaces\n\nSwiftConfigs supports namespace-based organization of configuration keys, providing compile-time structure and type safety for logically related keys.\n\n### Basic Namespaces\n\nGroup related keys in namespace extensions of `Configs.Keys`:\n\n```swift\npublic extension Configs.Keys {\n\n    var security: Security { Security() }\n    struct Security: ConfigNamespaceKeys {}\n}\n\nextension Configs.Keys.Security {\n\n    public var apiToken: RWConfigKey\u003cString?\u003e {\n        ConfigKey(\"api-token\", in: .secure)\n    }\n        \n    public var encryptionEnabled: ROConfigKey\u003cBool\u003e {\n        ConfigKey(\"encryption-enabled\", in: .secure, default: true)\n    }\n}\n\n// Usage - clean, organized access\nlet configs = Configs()\nlet apiToken = configs.security.apiToken\nconfigs.security.encryptionEnabled = false\n\n// Property wrapper usage\n@RWConfig(\\.security.apiToken) var token: String?\n@ROConfigState(\\.security.encryptionEnabled) var isEncryptionEnabled: Bool\n```\n\n### Nested Namespaces\n\nCreate deeper hierarchies by nesting namespace types:\n\n```swift\npublic extension Configs.Keys {\n\n    var features: Features { Features() }\n    struct Features: ConfigNamespaceKeys {\n    \n        public var auth: Auth { Auth() }\n        public struct Auth: ConfigNamespaceKeys {}\n    }\n}\n\nextension Configs.Keys.Features.Auth {\n\n    public var biometricEnabled: RWConfigKey\u003cBool\u003e {\n        ConfigKey(\"biometric-enabled\", in: .default, default: false)\n    }\n}\n\n// Usage - deep namespace navigation\nlet biometricEnabled = configs.features.auth.biometricEnabled\nconfigs.features.auth.biometricEnabled = true\n```\n\n### Key Prefixing (Optional)\n\nNamespaces are primarily for code organization. But if needed, you can add a `keyPrefix` to automatically prefix all keys in that namespace:\n\n```swift\npublic extension Configs.Keys {\n\n    var environment: Environment { Environment() }\n\n     struct Environment: ConfigNamespaceKeys {\n        public var keyPrefix: String { \"env/\" }  // Optional key prefixing\n        \n        public var apiUrl: ROConfigKey\u003cString\u003e {\n            ConfigKey(qualify(\"api-url\"), in: .environment, default: \"localhost\") // \"env/api-url\"\n        }\n    }\n}\n```\n\n## Async/Await Support\n\n```swift\nlet configs = Configs()\n\n// Fetch latest values\ntry await configs.fetch()\n\n// Fetch and get specific value\nlet token = try await configs.fetch(configs.apiToken)\n\n// Fetch only if needed\nlet value = try await configs.fetchIfNeeded(configs.someKey)\n```\n\n## Listening for Changes\n\n### Callback-based Listening\n\n```swift\nlet configs = Configs()\n\n// Listen to all configuration changes\nlet cancellation = configs.onChange { configs in\n    print(\"Configurations updated\")\n}\n\n// Listen to specific key changes  \nlet keyCancellation = configs.onChange(\\.apiToken) { newToken in\n    print(\"API token changed: \\(newToken)\")\n}\n\n// Cancel when done\ncancellation.cancel()\nkeyCancellation.cancel()\n```\n\n### Async Sequence-based Listening\n\n```swift\nlet configs = Configs()\n\n// Listen to all configuration changes using async sequences\nfor await updatedConfigs in configs.changes() {\n    print(\"Configurations updated\")\n}\n\n// Listen to specific key changes using async sequences\nfor await newToken in configs.changes(for: \\.apiToken) {\n    print(\"API token changed: \\(newToken)\")\n}\n\n// Use in async context with cancellation\nlet task = Task {\n    for await newToken in configs.changes(for: \\.apiToken) {\n        print(\"API token changed: \\(newToken)\")\n        // Break on specific condition\n        if newToken == \"expected-token\" {\n            break\n        }\n    }\n}\n\n// Cancel the task when needed\ntask.cancel()\n```\n\n### Combine Publisher Support\n\nWhen Combine is available, configuration changes can also be used as Publishers:\n\n```swift\nimport Combine\n\nlet configs = Configs()\nvar cancellables = Set\u003cAnyCancellable\u003e()\n\n// Listen to configuration changes using Combine\nconfigs.changes()\n    .sink { updatedConfigs in\n        print(\"Configurations updated\")\n    }\n    .store(in: \u0026cancellables)\n\n// Listen to specific key changes using Combine\nconfigs.changes(for: \\.apiToken)\n    .sink { newToken in\n        print(\"API token changed: \\(newToken)\")\n    }\n    .store(in: \u0026cancellables)\n\n// Chain with other Combine operators\nconfigs.changes(for: \\.apiToken)\n    .compactMap { $0 }\n    .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)\n    .sink { debouncedToken in\n        print(\"Debounced API token: \\(debouncedToken)\")\n    }\n    .store(in: \u0026cancellables)\n```\n\n## Value Transformers\n\nSwiftConfigs automatically handles common types:\n\n```swift\npublic extension Configs.Keys {\n    \n    // String-convertible types\n    var count: ROConfigKey\u003cInt\u003e { \n        ConfigKey(\"count\", in: .default, default: 0)\n    }\n    \n    var rate: ROConfigKey\u003cDouble\u003e {\n        ConfigKey(\"rate\", in: .default, default: 1.0)\n    }\n    \n    // Enum types\n    var theme: ROConfigKey\u003cTheme\u003e { \n        ConfigKey(\"theme\", in: .default, default: .light)\n    }\n    \n    // Codable types (stored as JSON)\n    var settings: ROConfigKey\u003cAppSettings\u003e {\n        ConfigKey(\"settings\", in: .default, default: AppSettings())\n    }\n    \n    // Optional types\n    var optionalValue: ROConfigKey\u003cString?\u003e {\n        ConfigKey(\"optional\", in: .default)\n    }\n    \n    // Using specific stores when needed\n    var tempSetting: RWConfigKey\u003cString\u003e {\n        ConfigKey(\"temp\", store: .inMemory, default: \"temp-value\")\n    }\n    \n    var secureToken: RWConfigKey\u003cString?\u003e {\n        ConfigKey(\"secure-token\", store: .keychain)\n    }\n}\n```\n\n## Configuration Migration\n\nHandle configuration schema changes gracefully:\n\n```swift\npublic extension Configs.Keys {\n    \n    // Migrate from old boolean to new enum\n    var notificationStyle: ROConfigKey\u003cNotificationStyle\u003e {\n        ConfigKey(\"notification-style\", in: .default, default: .none)\n    }\n    \n    private var oldNotificationsEnabled: ROConfigKey\u003cBool\u003e {\n        ConfigKey(\"notifications-enabled\", in: .default, default: false)\n    }\n    \n    // Custom migration using multiplex stores can be done at bootstrap level:\n    // ConfigSystem.bootstrap([\n    //     .default: .multiple(.userDefaults, .inMemory) // Check multiple sources\n    // ])\n}\n```\n\n## Custom Configuration Stores\n\nCreate custom storage backends by implementing the `ConfigStore` protocol:\n\n```swift\nimport Foundation\n\nstruct MyCustomStore: ConfigStore {\n    var isWritable: Bool { true }\n    \n    func fetch(completion: @escaping (Error?) -\u003e Void) {\n        // Fetch latest values from your backend\n        completion(nil)\n    }\n    \n    func onChange(_ listener: @escaping () -\u003e Void) -\u003e Cancellation {\n        // Set up change notifications\n        return Cancellation { /* cleanup */ }\n    }\n    \n    func onChangeOfKey(_ key: String, _ listener: @escaping (String?) -\u003e Void) -\u003e Cancellation {\n        // Set up key-specific change notifications\n        return Cancellation { /* cleanup */ }\n    }\n    \n    func get(_ key: String) throws -\u003e String? {\n        // Retrieve value for key\n        return myDatabase.getValue(key)\n    }\n    \n    func set(_ value: String?, for key: String) throws {\n        // Store value for key\n        if let value = value {\n            myDatabase.setValue(value, forKey: key)\n        } else {\n            myDatabase.removeValue(forKey: key)\n        }\n    }\n    \n    func exists(_ key: String) throws -\u003e Bool {\n        return myDatabase.hasValue(forKey: key)\n    }\n    \n    func removeAll() throws {\n        myDatabase.clearAll()\n    }\n    \n    func keys() -\u003e Set\u003cString\u003e? {\n        return Set(myDatabase.allKeys())\n    }\n}\n\n// Use your custom store\nConfigSystem.bootstrap([\n    .default: MyCustomStore(),\n    .secure: .keychain\n])\n```\n\n## Available Implementations\n\nThere is a ready-to-use ConfigStore implementation:\n\n### Firebase Remote Config\n- **Repository**: [swift-firebase-tools](https://github.com/dankinsoid/swift-firebase-tools)\n- **Features**: Remote configuration management, A/B testing, real-time updates\n- **Use case**: Server-controlled feature flags and configuration values\n\n```swift\n// Add to Package.swift\n.package(url: \"https://github.com/dankinsoid/swift-firebase-tools.git\", from: \"0.3.0\")\n\n// Usage\nimport FirebaseConfigs\n\nConfigSystem.bootstrap([\n    .default: .userDefaults,\n    .remote: .firebaseRemoteConfig\n])\n```\n\n### Community Contributions\n\nWant to add your own ConfigStore implementation? Consider contributing to the ecosystem by:\n\n1. Creating a separate package with your store\n2. Following the `ConfigStore` protocol\n3. Adding comprehensive tests and documentation\n4. Submitting your package for inclusion in this list\n\n## Installation\n\n### Swift Package Manager\n\nAdd SwiftConfigs to your `Package.swift`:\n\n```swift\n// swift-tools-version:5.7\nimport PackageDescription\n\nlet package = Package(\n    name: \"YourProject\",\n    dependencies: [\n        .package(url: \"https://github.com/dankinsoid/swift-configs.git\", from: \"1.0.0\")\n    ],\n    targets: [\n        .target(name: \"YourProject\", dependencies: [\"SwiftConfigs\"])\n    ]\n)\n```\n\nOr add it through Xcode:\n1. Go to File → Add Package Dependencies\n2. Enter: `https://github.com/dankinsoid/swift-configs.git`\n3. Choose the version and add to your target\n\n## Best Practices\n\n1. **Define keys as computed properties** in `Configs.Keys` extensions for organization and discoverability\n2. **Use namespaces for organization** - group related keys into `ConfigNamespaceKeys` types for compile-time structure\n3. **Use appropriate categories** for different security and persistence needs\n4. **Provide sensible defaults** for all configuration keys\n5. **Use read-only keys (`ROConfigKey`)** when values shouldn't be modified at runtime\n6. **Bootstrap the system early** in your app lifecycle before accessing any configuration\n7. **Prefer category-based initialization** (`init(_:in:default:)`) over store-based for most use cases\n8. **Use store-based initialization** (`init(_:store:default:)`) only when you need specific store targeting or before system bootstrap\n9. **Use prefixing sparingly** - only add `keyPrefix` when you need it; most namespaces work fine with the default empty prefix\n10. **Handle migration** using multiplex stores or custom migration logic\n11. **Use property wrappers** for clean SwiftUI and declarative code integration\n12. **Leverage async/await** for remote configuration fetching\n13. **Use change observation** for reactive configuration updates\n\n## Security Considerations\n\n- Use **`.secure`** category for sensitive data (API tokens, passwords) - uses Keychain encryption\n- Use **`.critical`** for maximum security with hardware-backed Secure Enclave protection\n- Use **`.syncedSecure`** carefully - only for data that should be shared across devices via iCloud Keychain\n- **Never log** configuration values that might contain sensitive data\n- **Environment variables** are read-only and visible to the entire process and system\n- **Keychain accessibility levels** control when encrypted data can be accessed (device locked/unlocked)\n- **Biometric authentication** adds an extra layer of security for critical configuration data\n- **iCloud sync** (`.ubiquitous`) has a 1MB total storage limit and is eventually consistent\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\nSwiftConfigs is available under the MIT license. See the LICENSE file for more info.\n\n## Author\n\n**Daniil Voidilov**\n- Email: voidilov@gmail.com\n- GitHub: [@dankinsoid](https://github.com/dankinsoid)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdankinsoid%2Fswift-configs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdankinsoid%2Fswift-configs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdankinsoid%2Fswift-configs/lists"}