SwiftConfigs - type-safe unified API for small key-value storages

I’d like to share a new library: SwiftConfigs

It provides a unified, type-safe API for small key-value storage systems (UserDefaults, Keychain, environment variables, in-memory, iCloud key-value store, custom backends, etc.).

Instead of juggling different APIs, you define strongly-typed keys once and access them the same way across all backends:

public extension Configs.Keys {
    var apiToken: RWKey<String?> {
        RWKey("api-token", in: .secure, default: nil)
    }

    var serverURL: ROKey<String> {
        ROKey("SERVER_URL", in: .environment, default: "https://api.example.com")
    }
}

let configs = Configs()

// Direct API
configs.apiToken = "new-token"
print(configs.serverURL)


// --- Property wrappers ---

// 1. By keyPath
struct Session {
    @RWConfig(\.apiToken) var apiToken
}

var session = Session()
session.apiToken = "wrapped-token"
print(session.apiToken)

// 2. Directly with a key
struct Environment {
    @ROConfig("SERVER_URL", in: .environment) var serverURL = "https://api.example.com"
}

let env = Environment()
print(env.serverURL)

Highlights:

  • Works with UserDefaults, Keychain, NSUbiquitousKeyValueStore, in-memory, and environment variables out of the box

  • Custom backends supported (example: Firebase Remote Configs)

  • Type-safe keys for any Codable type

  • Per-key customization (store, transformer, migration)

  • Property wrappers and async/await support

  • Change observation via callbacks or async sequences

  • Migration helpers and multiplex stores

  • Stores are abstracted by categories for flexibility, testing, and previews

  • You can override a category with a specific store when needed

9 Likes

Here’s a small trick I’ve found useful when working with Firebase Remote Configs in debug builds.

It lets me override values locally while still pulling from Firebase.

if isDevelopBuild {
    configs.configSettings.minimumFetchInterval = 5

    let firebaseStore = FirebaseRemoteConfigStore(config: configs, allKeysKey: allKeysKey)
    caches[.remote] = .migration(
        from: firebaseStore,
        to: .prefix("debug.remote.configs.", store: .userDefaults)
    )
} else {
    configs.configSettings.minimumFetchInterval = 600

    let firebaseStore = FirebaseRemoteConfigStore(config: configs, allKeysKey: allKeysKey)
    caches[.remote] = firebaseStore
}

ConfigsSystem.bootstrap(caches)

With this setup:

  • In debug builds I can override any Firebase Remote Config with a UserDefaults value (prefixed debug.remote.configs.).

  • In release builds it just uses Firebase.

This made it much easier to test different feature flag setups locally via debug menu without changing them in Firebase.

Very nice! This plugs a big hole in the ecosystem. I haven’t looked in depth yet, but I expect this will also be really helpful for CLI tools and cloud services

1 Like