{"id":17923167,"url":"https://github.com/rogerluan/featureconfig","last_synced_at":"2025-04-03T09:17:01.970Z","repository":{"id":102915638,"uuid":"539929812","full_name":"rogerluan/FeatureConfig","owner":"rogerluan","description":"⚙️ An extensible feature flag and remote config service.","archived":false,"fork":false,"pushed_at":"2022-10-09T22:32:42.000Z","size":52,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-08T22:43:13.626Z","etag":null,"topics":["ios","spm","swift"],"latest_commit_sha":null,"homepage":"https://github.com/rogerluan/FeatureConfig","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rogerluan.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}},"created_at":"2022-09-22T10:43:18.000Z","updated_at":"2024-08-03T16:21:43.000Z","dependencies_parsed_at":null,"dependency_job_id":"881566c8-1a3f-4c75-b162-37e57b0294ad","html_url":"https://github.com/rogerluan/FeatureConfig","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogerluan%2FFeatureConfig","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogerluan%2FFeatureConfig/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogerluan%2FFeatureConfig/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogerluan%2FFeatureConfig/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rogerluan","download_url":"https://codeload.github.com/rogerluan/FeatureConfig/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246970326,"owners_count":20862509,"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","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":["ios","spm","swift"],"created_at":"2024-10-28T20:42:23.937Z","updated_at":"2025-04-03T09:17:01.943Z","avatar_url":"https://github.com/rogerluan.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg width=200 src=\"docs/logo.png\"\u003e\n  \u003ch1\u003eFeatureConfig\u003c/h1\u003e\n  \u003cp\u003e\u003cstrong\u003e⚙️ An extensible feature flag and remote config service.\u003c/strong\u003e\u003c/p\u003e\n\n  \u003ca href=\"https://github.com/rogerluan/FeatureConfig/actions/workflows/run_tests.yml\"\u003e\n    \u003cimg src=\"https://github.com/rogerluan/FeatureConfig/workflows/Run%20Tests/badge.svg\" alt=\"GitHub Action Build Status\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://swift.org\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/Swift-5.3-F05138?logo=swift\u0026logoColor=white\" alt=\"Swift 5.3\" /\u003e\n  \u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/static/v1?label=Platforms\u0026message=iOS%20|%20macOS%20|%20tvOS%20|%20watchOS%20|%20Ubuntu%20\u0026color=brightgreen\" alt=\"Supports iOS, macOS, tvOS, watchOS and Ubuntu\" /\u003e\n  \u003ca href=\"https://github.com/rogerluan/FeatureConfig/releases\"\u003e\n    \u003cimg alt=\"Latest Release\" src=\"https://img.shields.io/github/v/release/rogerluan/FeatureConfig?sort=semver\"\u003e\n  \u003c/a\u003e\n  \u003c/br\u003e\n  \u003ca href=\"https://codeclimate.com/github/rogerluan/FeatureConfig/maintainability\"\u003e\n    \u003cimg src=\"https://api.codeclimate.com/v1/badges/465cfeb965d52d65ba33/maintainability\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://codeclimate.com/github/rogerluan/FeatureConfig/test_coverage\"\u003e\n    \u003cimg src=\"https://api.codeclimate.com/v1/badges/465cfeb965d52d65ba33/test_coverage\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://twitter.com/intent/follow?screen_name=rogerluan_\"\u003e\n    \u003cimg src=\"https://img.shields.io/twitter/follow/rogerluan_?\u0026logo=twitter\" alt=\"Follow on Twitter\"\u003e\n  \u003c/a\u003e\n\n  \u003cp align=\"center\"\u003e\n    \u003ca href=\"https://github.com/rogerluan/FeatureConfig/issues/new/choose\"\u003eReport Bug\u003c/a\u003e\n    ·\n    \u003ca href=\"https://github.com/rogerluan/FeatureConfig/issues/new/choose\"\u003eRequest Feature\u003c/a\u003e\n  \u003c/p\u003e\n\u003c/div\u003e\n\n# Why would I use this?\n\nFollowing the principles of [iOS Factor](https://ios-factor.com), a methodology to write high-quality iOS applications, more specifically the [Config factor](https://ios-factor.com/config), there should be no configuration in code: the app must ship with a default configuration, and allow OTA updates. This package provides an entry point for those OTA updates, which can be used, for instance, to:\n\n- Run A/B tests to enable certain features or UI changes only for a subset of the active users;\n- Rotate API keys;\n- Remotely disable features;\n- Conduct phased rollout of features.\n\n# Installation\n\nUsing Swift Package Manager:\n\n```swift\ndependencies: [\n    .package(name: \"FeatureConfig\", url: \"https://github.com/rogerluan/FeatureConfig\", .upToNextMajor(from: \"1.0.0\")),\n]\n```\n\n# Terminology\n\nThe concept of feature flags is widely spread and well known. Some services call them feature flags, others call it remote config, and some make a distinction between flags and configs. This package unifies all those concepts in one: _Feature Configs_, regardless of what the \"config\" might be, or where it's being read from: it could be a boolean (to enable/disable a feature), a string, or even a complex key-value structure, and it could also be fetched from a remote server or service, as well as be loaded from local files or static default configs.\n\n# Usage\n\n## Providers\n\nFeatureConfig was implemented with extensibility by design. It consumes concrete implementations of the `Provider` protocol as its data source, and will fetch information from them upon initialization. FeatureConfig comes with only 1 provider out-of-the-box, `EphemeralProvider`, which conforms to the `MutableProvider` protocol, meaning you can modify its configs during runtime (set their values), whereas `Provider` only allows you to read from them (since they will be written to by another service). `EphemeralProvider` should be used only for debugging purposes.\n\nFor an example on how to implement your own `Provider`, see: https://github.com/rogerluan/FeatureConfig_LaunchDarkly/blob/main/Sources/LaunchDarklyProvider.swift\n\nHere is a list of community-driven Swift Packages that were already implemented to connect to feature config services:\n\n- [LaunchDarkly](https://launchdarkly.com): https://github.com/rogerluan/FeatureConfig_LaunchDarkly\n\nIf you have implemented a provider that other people could benefit from, feel free to open a PR modifying the list above 🤗\n\nHere is some inspiration:\n\n- Implementing your own solution\n- JSON-based open source solutions, like [FeatureFlags](https://github.com/rwbutler/FeatureFlags)\n- Proprietary services, like [Firebase Remote Config](https://firebase.google.com/docs/remote-config)\n- Proprietary services that allow running on-prem, like [Flagsmith](https://flagsmith.com)\n\n## Initializing\n\nOnce you have implemented your providers, it's time to initialize FeatureConfig:\n\n```swift\nimport FeatureConfig\nimport UIKit\n\n@main\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -\u003e Bool {\n        // Override point for customization after application launch.\n        var providers: [Provider] = [ /* Configure your providers here */ ]\n        #if DEBUG\n        providers.append(EphemeralProvider())\n        #endif\n        FeatureConfig.initialize(providers: providers)\n        return true\n    }\n\n    // […]\n}\n```\n\n## Reading Configs\n\nWhen implementing a feature, you should strive to always put this feature behind a feature config. This config should generally be \"disabled\" by default, while you enable it on your server-side service. This provides a _safe_ way to ship new features, run A/B tests on them, or even conduct a phased rollout.\n\nTo add a new feature config, use the `@Feature` property wrapper in an instance or static variable, as needed. For example:\n\n```swift\nimport FeatureConfig\n\nclass MyView {\n    @Feature(\"is_super_like_enabled\", default: false) private var isSuperLikeEnabled: Bool\n\n    func setUpSuperLike() {\n        guard isSuperLikeEnabled else { return }\n        view.addSubview(superLikeButton)\n    }\n}\n```\n\nNotice how easy it is to put your entire feature behind a feature config. It's a one-liner to configure it, and another one-liner to use it.\n\nThe `@Feature` property wrapper takes two arguments:\n\n```swift\n/// Instantiates a feature config property wrapper.\n/// - Parameters:\n///   - key: the key of this feature config, as configured in the providers' end.\n///   - defaultExpression: the fallback value of this feature, in case none of the providers are able to\n///   provide a valid value for this feature during runtime. This expression is evaluated only when and if needed.\npublic init(_ key: String, default defaultExpression: @autoclosure @escaping () -\u003e Value)\n```\n\nThe `Value` of a feature config must conform to `Codable`, thus it can be any of the existing Foundation types that already conform to `Codable`, but also more complex [`JSEN`](https://github.com/rogerluan/JSEN) values, and even custom structures, as long as conformance to `Codable` is put in place.\n\n## Observing Changes\n\nYou can observe changes of your providers' `configs` instance properties as well as `FeatureConfig`'s shared instance, using Combine observers, as `configs` is a `CurrentValueSubject`. This is the designed way to know when the config values have been updated.\n\n# Architecture Decisions\n\n## Lifecycle\n\nThe lifecycle of a feature config value goes as follows:\n\n1. On the user's first app launch (right after install), the app doesn't know anything about remote feature configs, thus the binary is shipped with enough information so that it can be used without the need to download extra resources from remote servers (such as config settings, API keys, etc). This is in accordance to the [iOS Factor's \"Prefer local over Remote\" factor](https://ios-factor.com/prefer-local-over-remote), where it's stipulated that the app should be smart enough to operate without a backend where possible, and that we should never assume a user has a working internet connection on the first launch of the app.\n2. Once the app reaches internet connection for the first time, it will fetch remote configurations for its feature config providers. This information presumably cached (by the service itself, not FeatureConfig) and from then on, any further \"offline interactions\" will use the latest known cached config values.\n3. The responsibility of fetching the values from your service belongs to the provider. Thus, determining _when_ it should fetch the remote values (e.g. during app launch, or when a different user authenticates) is the app's responsibility, by calling, for instance a `refresh()` method on the provider, when needed.\n\n## Real-time Updates\n\nThe feature config system was specifically designed to prevent feature configs from receiving remote updates during runtime as much as possible. This means it actively prevents configs from receiving updates via real-time mechanisms such as websocket events. This is because the higher the chance the feature configs have to change during any arbitrary moment in the app's lifecycle, the more likely it is for a user to get into a unpredictable state. This way we attempt to eliminate an entire class of bugs that would be hard to understand, reproduce and debug.\n\n## Caching\n\nFeatureConfig doesn't implement caching on its own. Caching is usually built by SDKs of services being used (which map to your providers), so this usually means there's no need to implement a secondary caching mechanism. If the services being used don't implement a caching mechanism, you may implement one in your provider.\n\n# Resources\n\nFor an introduction to the concept of feature configs, see: https://betterprogramming.pub/feature-configs-5ff6be0a4568\n\nOther interesting resources:\n\n- https://andreaslydemann.com/clean-ios-architecture-for-feature-toggling/\n- https://jeroenmols.com/blog/2019/09/12/FeatureConfigsarchitecture/\n- https://martinfowler.com/articles/feature-toggles.html\n\n# Contributions\n\nIf you spot something wrong, missing, or if you'd like to propose improvements to this project, please open an Issue or a Pull Request with your ideas and I promise to get back to you within 24 hours! 😇\n\n# License\n\nThis project is open source and covered by a standard 2-clause BSD license. That means you can use (publicly, commercially and privately), modify and distribute this project's content, as long as you mention **Roger Oba** as the original author of this code and reproduce the LICENSE text inside your app, repository, project or research paper.\n\n# Contact\n\nTwitter: [@rogerluan_](https://twitter.com/rogerluan_)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogerluan%2Ffeatureconfig","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frogerluan%2Ffeatureconfig","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogerluan%2Ffeatureconfig/lists"}