Skip to content

harryngict/FeatureToggleKit

Repository files navigation

FeatureToggleKit

Swift Version Platform License SPM Compatible CocoaPods Compatible

A production-ready, modular Swift feature toggle/flag management library for iOS applications, powered by Firebase Remote Config. FeatureToggleKit provides a clean, protocol-oriented architecture with separate modules for interfaces, implementation, and testingβ€”demonstrating SOLID principles and Clean Architecture best practices.


πŸš€ Why This Library Stands Out

πŸ”„ Zero Vendor Lock-in Architecture
Swap Firebase for LaunchDarkly, Split.io, or your custom backend with ONE line of code change. Your entire app depends on interfaces, not implementations. No breaking changes. No feature module rewrites. Just true architectural flexibility.

// Change this ONE line to switch providers - that's it!
let featureToggle: FeatureToggleKit = FeatureToggleKitImp()      // Firebase
let featureToggle: FeatureToggleKit = LaunchDarklyToggleKit()    // LaunchDarkly  
let featureToggle: FeatureToggleKit = YourCustomProvider()       // Your backend
// βœ… All feature modules keep working - ZERO changes needed!

Perfect for: Enterprise apps that need provider flexibility, teams concerned about vendor lock-in, or projects that may need to migrate providers in the future.

πŸ“– See full provider flexibility examples below ↓


🎯 Features

Core Capabilities

  • βœ… Type-safe feature toggles with support for multiple value types (Bool, String, Int64, Double, JSON)
  • βœ… Firebase Remote Config integration for dynamic feature management without app releases
  • βœ… Real-time updates via listener pattern for instant feature flag changes
  • βœ… Local override (Tweaks) for QA testing and development
  • βœ… Thread-safe operations with Swift 6 strict concurrency support
  • βœ… Zero-dependency interface for feature modules

Architecture & Design

  • πŸ—οΈ Clean Architecture with clear separation of concerns
  • πŸ”Œ Dependency Injection ready - perfect for modular apps
  • πŸ§ͺ 100% testable with comprehensive mock implementations
  • πŸ“¦ Modular design - use only what you need (Interface, Implementation, Mock)
  • 🎯 Protocol-oriented following Swift best practices
  • πŸ”’ Type-safe API prevents runtime errors

πŸ“¦ Modules

FeatureToggleKit follows the Interface Segregation Principle and is organized into three distinct modules for maximum flexibility and minimal coupling:

1. FeatureToggleKit (Interface Module) 🎯

The interface module contains all protocols and data models. This module should be injected into your feature modules to maintain loose coupling.

Key Benefits:

  • βœ… Zero dependencies (no Firebase, no external frameworks)
  • βœ… Lightweight - perfect for feature modules
  • βœ… Enables true dependency injection
  • βœ… Makes your feature modules independently testable

Contains:

  • FeatureToggleKit - Main protocol for accessing feature toggles
  • FeatureToggleDefinition - Defines a feature toggle with key and default value
  • FeatureToggleValue - Type-safe enum representing different value types
  • FeatureToggleKitListener - Protocol for receiving toggle update notifications

2. FeatureToggleKitImp (Implementation Module) βš™οΈ

The implementation module provides production-ready concrete implementations using Firebase Remote Config. Initialize this in your main app and inject it to feature modules.

Key Benefits:

  • βœ… Firebase Remote Config integration for remote feature management
  • βœ… Thread-safe local caching with UserDefaults persistence
  • βœ… Observable pattern with listener management
  • βœ… Tweak/override support for QA and debugging
  • βœ… Async/await Swift 6 concurrency support

3. FeatureToggleKitMock (Mock Module) πŸ§ͺ

Mock implementations for unit testing. Use this module in your test targets to write isolated, fast tests without Firebase dependencies.

Key Benefits:

  • βœ… No network calls - fast test execution
  • βœ… Predictable behavior - no flaky tests
  • βœ… Full control over test scenarios
  • βœ… Works with any testing framework (XCTest, Quick/Nimble, etc.)

Contains:

  • FeatureToggleKitMock - Mock implementation with configurable return values
  • FeatureToggleKitListenerMock - Mock listener for testing notifications
  • Full protocol conformance for drop-in replacement

πŸ“‹ Requirements

  • iOS 15.0+
  • Swift 6.0+
  • Xcode 16.0+
  • Firebase Remote Config 12.3.0+

πŸš€ Installation

Swift Package Manager

Add FeatureToggleKit to your Package.swift:

dependencies: [
    .package(url: "https://github.com/harryngict/FeatureToggleKit.git", from: "0.0.0")
]

Then add the appropriate modules to your targets:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "FeatureToggleKitImp", package: "FeatureToggleKit")
    ]
),
.target(
    name: "YourFeatureModule",
    dependencies: [
        .product(name: "FeatureToggleKit", package: "FeatureToggleKit")
    ]
),
.testTarget(
    name: "YourTests",
    dependencies: [
        .product(name: "FeatureToggleKitMock", package: "FeatureToggleKit")
    ]
)

CocoaPods

Add to your Podfile:

# Main app target
target 'YourApp' do
  pod 'FeatureToggleKitImp'
end

# Feature module target
target 'YourFeatureModule' do
  pod 'FeatureToggleKit'
end

# Test target
target 'YourAppTests' do
  pod 'FeatureToggleKitMock'
end

πŸ’‘ Usage

Basic Setup (Main App)

Initialize FeatureToggleKit in your app's entry point:

import FeatureToggleKitImp
import FirebaseCore

// Configure Firebase
FirebaseApp.configure()

// Create feature toggle definitions
let definitionProvider = YourDefinitionProvider()

// Initialize the implementation
 let featureToggleKit = FeatureToggleKitImp(tweakCacheService: TweakCacheServiceImp(cacheType: .both))
featureToggleKit.setup(definitionProviders: [definitionProvider])

// Inject to your feature modules
let featureModule = FeatureModule(featureToggleKit: featureToggleKit)

Defining Feature Toggles

Create a definition provider:

enum YourFeatureToggleDefinition: String, CaseIterable {
  case testFeatureToggleBool = "test_bool_enabled"
  case testFeatureToggleInt = "test_int_value"
  case testFeatureToggleDouble = "test_double_value"
  case testFeatureToggleString = "test_string_value"
  case testFeatureToggleJSON = "test_json_value"

  var defaultValue: FeatureToggleValue {
    switch self {
    case .testFeatureToggleBool: return false
    case .testFeatureToggleInt: return 0
    case .testFeatureToggleDouble: return 0.0
    case .testFeatureToggleString: return ""
    case .testFeatureToggleJSON: return .json(value: [:])
    }
  }
}

struct YourDefinitionProvider: FeatureToggleDefinitionProvider {
  init() {}

  let name = "Harry iOS Team"

  var definitions: [FeatureToggleDefinition] {
    YourFeatureToggleDefinition.allCases.map { definition in
      FeatureToggleDefinition(
        key: definition.rawValue,
        defaultValue: definition.defaultValue)
    }
  }
}

Using in Feature Modules (Interface Only)

Feature modules only depend on the interface:

import FeatureToggleKit

class FeatureModule {
    private let featureToggleKit: FeatureToggleKit
    
    init(featureToggleKit: FeatureToggleKit) {
        self.featureToggleKit = featureToggleKit
    }
    
    func checkFeature() {
        // Get boolean value
        let isEnabled = featureToggleKit.getBoolValue(key: "new_feature_enabled")
        
        // Get string value with fallback
        let message = featureToggleKit.getStringValue(
            key: "welcome_message", 
            fallbackValue: "Default Message"
        )
        
        // Get numeric values
        let timeout = featureToggleKit.getLongValue(key: "api_timeout")
        let multiplier = featureToggleKit.getDoubleValue(key: "price_multiplier")
        
        // Get JSON configuration
        let config = featureToggleKit.getJSONValue(key: "feature_config")
    }
}

Listening to Updates

Implement the listener protocol to receive real-time updates:

import FeatureToggleKit

class FeatureObserver: FeatureToggleKitListener {
    func didReceiveFeatureToggleUpdates(values: [(String, FeatureToggleValue)]) async {
        for (key, value) in values {
            print("Feature \(key) updated to \(value)")
        }
    }
    
    func didReceiveError(error: Error?) async {
        print("Error receiving updates: \(String(describing: error))")
    }
}

// Add listener
let observer = FeatureObserver()
featureToggleKit.addListener(observer)

// Remove when done
featureToggleKit.removeListener(observer)

Unit Testing with Mocks

The FeatureToggleKitMock module provides comprehensive mock implementations for testing. All mocks are thread-safe and Sendable, supporting Swift 6 strict concurrency.

Basic Mock Setup

import XCTest
import FeatureToggleKit
import FeatureToggleKitMock

class FeatureModuleTests: XCTestCase {
    var mockToggleKit: FeatureToggleKitMock!
    
    override func setUp() {
        super.setUp()
        mockToggleKit = FeatureToggleKitMock()
    }
    
    func testFeatureWithToggleEnabled() {
        // Configure mock behavior using handler
        mockToggleKit.getBoolValueHandler = { key, fallback in
            return key == "new_feature_enabled" ? true : fallback
        }
        
        // Test your feature
        let feature = FeatureModule(featureToggleKit: mockToggleKit)
        let isEnabled = feature.checkNewFeature()
        
        // Assertions
        XCTAssertTrue(isEnabled)
        XCTAssertEqual(mockToggleKit.getBoolValueCallCount, 1)
    }
}

Configuring Mock Handlers

Each mock method has a corresponding handler property for custom behavior:

// Boolean values
mockToggleKit.getBoolValueHandler = { key, fallback in
    switch key {
    case "premium_enabled": return true
    case "beta_features": return false
    default: return fallback
    }
}

// String values
mockToggleKit.getStringValueHandler = { key, fallback in
    return key == "api_url" ? "https://test.api.com" : fallback
}

// Numeric values
mockToggleKit.getLongValueHandler = { key, fallback in
    return key == "timeout" ? 30 : fallback
}

mockToggleKit.getDoubleValueHandler = { key, fallback in
    return key == "price_multiplier" ? 1.5 : fallback
}

// JSON configuration
mockToggleKit.getJSONValueHandler = { key, fallback in
    if key == "app_config" {
        return ["theme": "dark", "version": "2.0"]
    }
    return fallback
}

Verifying Method Calls

Track how many times each method was called:

func testFeatureCallsToggleCorrectly() {
    mockToggleKit.getBoolValueHandler = { _, _ in true }
    
    let feature = FeatureModule(featureToggleKit: mockToggleKit)
    feature.performAction()
    
    // Verify the feature toggle was checked
    XCTAssertEqual(mockToggleKit.getBoolValueCallCount, 1)
    
    feature.performAnotherAction()
    
    // Verify multiple calls
    XCTAssertEqual(mockToggleKit.getBoolValueCallCount, 2)
}

Testing Listener Functionality

Test real-time feature toggle updates:

func testFeatureTogglesListener() async {
    let expectation = XCTestExpectation(description: "Listener called")
    
    // Configure listener handler
    mockToggleKit.addListenerHandler = { listener in
        // Simulate receiving updates
        Task {
            await listener.didReceiveFeatureToggleUpdates(values: [
                ("new_feature", .bool(value: true))
            ])
            expectation.fulfill()
        }
    }
    
    let observer = FeatureObserver()
    mockToggleKit.addListener(observer)
    
    await fulfillment(of: [expectation], timeout: 1.0)
    XCTAssertEqual(mockToggleKit.addListenerCallCount, 1)
}

Testing Async Operations

Test async methods like updateLocal:

func testLocalOverride() async {
    var capturedKey: String?
    var capturedValue: FeatureToggleValue?
    
    mockToggleKit.updateLocalHandler = { key, value in
        capturedKey = key
        capturedValue = value
    }
    
    await mockToggleKit.updateLocal(
        key: "test_feature", 
        newValue: .bool(value: true)
    )
    
    XCTAssertEqual(capturedKey, "test_feature")
    XCTAssertEqual(capturedValue, .bool(value: true))
    XCTAssertEqual(mockToggleKit.updateLocalCallCount, 1)
}

Testing Setup and Teardown

func testFeatureToggleSetup() {
    let definitionProvider = TestDefinitionProvider()
    
    mockToggleKit.setupHandler = { providers in
        XCTAssertEqual(providers.count, 1)
        XCTAssertEqual(providers.first?.name, "TestProvider")
    }
    
    mockToggleKit.setup(definitionProviders: [definitionProvider])
    XCTAssertEqual(mockToggleKit.setupCallCount, 1)
}

func testClearCache() {
    var cacheClearedCalled = false
    
    mockToggleKit.clearCacheHandler = {
        cacheClearedCalled = true
    }
    
    mockToggleKit.clearCache()
    
    XCTAssertTrue(cacheClearedCalled)
    XCTAssertEqual(mockToggleKit.clearCacheCallCount, 1)
}

Advanced: Testing Tweak Functionality

func testGetFeatureToggleTweak() {
    let definition = FeatureToggleDefinition(
        key: "test_feature",
        defaultValue: .bool(value: false)
    )
    
    mockToggleKit.getFeatureToggleTweakHandler = { def in
        if def.key == "test_feature" {
            return FeatureToggleTweak(
                definition: def,
                isEnabled: true,
                tweakedValue: .bool(value: true)
            )
        }
        return nil
    }
    
    let tweak = mockToggleKit.getFeatureToggleTweak(definition)
    
    XCTAssertNotNil(tweak)
    XCTAssertTrue(tweak?.isEnabled ?? false)
    XCTAssertEqual(mockToggleKit.getFeatureToggleTweakCallCount, 1)
}

Protocol-Specific Mocks

For more granular testing, use protocol-specific mocks:

// Testing value retrieval only
let valueProviderMock = FeatureToggleValueProvidingMock()
valueProviderMock.getBoolValueHandler = { _, _ in true }

// Testing listener management only
let listenerMock = FeatureToggleListenerManagingMock()
listenerMock.addListenerHandler = { listener in
    // Custom logic
}

// Testing cache management only
let cacheMock = FeatureToggleTweakCacheManagingMock()
cacheMock.clearCacheHandler = {
    // Custom logic
}

Best Practices

  1. Always set handlers before testing: Without handlers, mocks return default values (false, "", 0, etc.)
  2. Use call counts for verification: Ensure your feature interacts with toggles as expected
  3. Test both enabled and disabled states: Cover all code paths
  4. Leverage protocol mocks: Use specific protocol mocks for focused unit tests
  5. Thread-safety: All mocks are @unchecked Sendable and thread-safe with internal locking
  6. No network calls: Mocks run entirely in-memory for fast, reliable tests

Complete Test Example

import XCTest
import FeatureToggleKit
import FeatureToggleKitMock

class CheckoutModuleTests: XCTestCase {
    var mockToggleKit: FeatureToggleKitMock!
    var checkoutModule: CheckoutModule!
    
    override func setUp() {
        super.setUp()
        mockToggleKit = FeatureToggleKitMock()
        checkoutModule = CheckoutModule(featureToggleKit: mockToggleKit)
    }
    
    func testCheckoutWithNewFlowEnabled() {
        // Given: New checkout flow is enabled
        mockToggleKit.getBoolValueHandler = { key, _ in
            return key == "new_checkout_flow"
        }
        
        // When: User proceeds to checkout
        let result = checkoutModule.proceedToCheckout()
        
        // Then: New flow is used
        XCTAssertTrue(result.usedNewFlow)
        XCTAssertEqual(mockToggleKit.getBoolValueCallCount, 1)
    }
    
    func testCheckoutWithNewFlowDisabled() {
        // Given: New checkout flow is disabled
        mockToggleKit.getBoolValueHandler = { _, fallback in fallback }
        
        // When: User proceeds to checkout
        let result = checkoutModule.proceedToCheckout()
        
        // Then: Old flow is used
        XCTAssertFalse(result.usedNewFlow)
        XCTAssertEqual(mockToggleKit.getBoolValueCallCount, 1)
    }
    
    func testCheckoutConfiguration() {
        // Given: Custom checkout config
        mockToggleKit.getJSONValueHandler = { key, _ in
            if key == "checkout_config" {
                return ["payment_methods": ["card", "paypal"], "timeout": 30]
            }
            return [:]
        }
        
        // When: Loading checkout config
        let config = checkoutModule.loadConfiguration()
        
        // Then: Config is properly loaded
        XCTAssertEqual(config.paymentMethods.count, 2)
        XCTAssertEqual(mockToggleKit.getJSONValueCallCount, 1)
    }
}

Advanced: Local Overrides (Tweaks)

Override feature toggle values locally for testing:

// Update local value
await featureToggleKit.updateLocal(
    key: "new_feature_enabled", 
    newValue: .bool(value: true)
)

// Get tweak for a definition
let definition = FeatureToggleDefinition(key: "new_feature_enabled", defaultValue: false)
if let tweak = featureToggleKit.getFeatureToggleTweak(definition) {
    tweak.isEnabled = true
    tweak.tweakedValue = .bool(value: false)
}

// Clear all local overrides
featureToggleKit.clearCache()

πŸ—οΈ Architecture

The modular architecture provides clear separation of concerns:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           Main App                      β”‚
β”‚  (FeatureToggleKitImp)                 β”‚
β”‚  - Initialize implementation            β”‚
β”‚  - Configure Firebase                   β”‚
β”‚  - Inject to feature modules            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚ inject
              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚       Feature Modules                   β”‚
β”‚  (FeatureToggleKit - Interface)        β”‚
β”‚  - Depend only on protocols             β”‚
β”‚  - Loosely coupled                      β”‚
β”‚  - Easy to test                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚ mock in tests
              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           Unit Tests                    β”‚
β”‚  (FeatureToggleKitMock)                β”‚
β”‚  - Mock implementations                 β”‚
β”‚  - Fast, isolated tests                 β”‚
β”‚  - No Firebase dependency               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Benefits of this Architecture:

  • Interface Module: Enables dependency injection and loose coupling (Dependency Inversion Principle)
  • Implementation Module: Provides real Firebase-backed implementation (Single Responsibility)
  • Mock Module: Facilitates fast, reliable unit testing without external dependencies
  • Provider Flexibility: Swap Firebase for LaunchDarkly/Split.io/custom backend with ZERO changes to feature modules
  • Vendor Independence: No lock-in to any specific provider - migrate freely
  • Team Velocity: Feature teams can work independently without main app dependencies
  • Compile-time Safety: Interface changes are caught at compile time across all modules

πŸ”„ Provider Flexibility: Swap Implementations Without Breaking Changes

Why This Matters

One of the most powerful aspects of FeatureToggleKit's architecture is provider flexibility. Your entire app depends on the interface, not the concrete Firebase implementation. This means:

// Today: Using Firebase Remote Config
let featureToggle: FeatureToggleKit = FeatureToggleKitImp() // Firebase implementation

// Tomorrow: Switch to LaunchDarkly - ZERO changes to feature modules!
let featureToggle: FeatureToggleKit = LaunchDarklyFeatureToggleKit()

// Or use your custom backend
let featureToggle: FeatureToggleKit = CustomBackendFeatureToggle()

// Your feature modules don't know or care!
class CheckoutModule {
    init(featureToggle: FeatureToggleKit) { // Still just the interface!
        // Works with ANY implementation
    }
}

Real-World Scenarios

Scenario 1: Vendor Migration

// Need to migrate from Firebase to LaunchDarkly? 
// Just implement the FeatureToggleKit protocol!

class LaunchDarklyFeatureToggleKit: FeatureToggleKit {
    // Implement the protocol methods
    func getBoolValue(key: String, fallbackValue: Bool) -> Bool {
        return ldClient.boolVariation(key: key, defaultValue: fallbackValue)
    }
    // ... other methods
}

// In your app initialization - ONE LINE CHANGE
// Before:
let featureToggle = FeatureToggleKitImp() // Firebase

// After:
let featureToggle = LaunchDarklyFeatureToggleKit() // LaunchDarkly

// βœ… All feature modules continue working without ANY changes!
// βœ… No recompilation of feature modules needed!
// βœ… Zero breaking changes!

Scenario 2: Multi-Tenant or Hybrid Solutions

// Use different providers for different environments or clients
let featureToggle: FeatureToggleKit = {
    switch environment {
    case .production:
        return FeatureToggleKitImp() // Firebase for most users
    case .enterprise:
        return CustomEnterpriseToggle() // Custom solution for enterprise
    case .testing:
        return FeatureToggleKitMock() // Mocks for automated tests
    }
}()

Scenario 3: Cost Optimization

// Start with expensive provider, migrate to cheaper one later
class CombinedFeatureToggleKit: FeatureToggleKit {
    // Use free UserDefaults for low-priority toggles
    // Use paid service only for critical feature flags
    func getBoolValue(key: String, fallbackValue: Bool) -> Bool {
        if criticalFeatures.contains(key) {
            return premiumProvider.getValue(key)
        }
        return userDefaultsProvider.getValue(key)
    }
}

Benefits for Enterprise

Benefit Impact
No Vendor Lock-in Switch providers without rewriting your app
Risk Mitigation Test new providers in parallel before full migration
Cost Control Move to cheaper alternatives as needed
Flexibility Use different providers per market/region/client
Future-Proof New providers can be added without breaking changes

Implementation Complexity

🎯 ARCHITECTURAL HIGHLIGHT
Single Point of Integration: To add a new provider (LaunchDarkly, Split.io, custom backend), you only need to:

  1. Implement the FeatureToggleService protocol (5 simple methods)
  2. Pass it to FeatureToggleProviderImp constructor

That's it! No changes to interfaces, no changes to feature modules, no changes to the rest of the implementation.


Step 1: Implement the FeatureToggleService Protocol

Firebase Remote Config currently implements this protocol. To use a different provider, just create your own implementation:

import FeatureToggleKit

// Current implementation uses Firebase
// File: FeatureToggleProviderImp.swift (line 10)
public init(featureToggleService: FeatureToggleService = RemoteConfig.remoteConfig()) {
    self.featureToggleService = featureToggleService
}

// The FeatureToggleService protocol (only 5 methods!)
protocol FeatureToggleService {
    func getFeatureToggleValue(_ definition: FeatureToggleDefinition) -> FeatureToggleValue?
    func addOnConfigUpdateListener(completion: @escaping (Set<String>?, Error?) -> Void)
    func activateWithCompletion(completion: @escaping (Bool, Error?) -> Void)
    func setDefaults(_ definition: FeatureToggleDefinition)
    func fetchAndActivate(completion: @escaping () -> Void)
}

Step 2: Create Your Provider Implementation

// Example: LaunchDarkly implementation
import LaunchDarkly
import FeatureToggleKit

class LaunchDarklyService: FeatureToggleService {
    private let ldClient: LDClient
    
    init(client: LDClient) {
        self.ldClient = client
    }
    
    func getFeatureToggleValue(_ definition: FeatureToggleDefinition) -> FeatureToggleValue? {
        // Map to LaunchDarkly API
        switch definition.defaultValue {
        case .bool:
            let value = ldClient.variation(forKey: definition.key, defaultValue: false)
            return .bool(value: value)
        case .string:
            let value = ldClient.variation(forKey: definition.key, defaultValue: "")
            return .string(value: value)
        // ... other types
        }
    }
    
    func addOnConfigUpdateListener(completion: @escaping (Set<String>?, Error?) -> Void) {
        // LaunchDarkly listener logic
        ldClient.observe { keys in
            completion(keys, nil)
        }
    }
    
    // Implement remaining 3 methods...
}

Step 3: Inject Your Provider (ONE Line Change!)

// Before: Using Firebase
let provider = FeatureToggleProviderImp(
    featureToggleService: RemoteConfig.remoteConfig()  // Firebase
)

// After: Using LaunchDarkly
let provider = FeatureToggleProviderImp(
    featureToggleService: LaunchDarklyService(client: ldClient)  // LaunchDarkly
)

// After: Using your custom backend
let provider = FeatureToggleProviderImp(
    featureToggleService: CustomBackendService(apiClient: myAPI)  // Your backend
)

// βœ… That's it! Everything else remains unchanged!

Why This is Brilliant

πŸ’‘ Single Point of Change: Only FeatureToggleProviderImp needs to know about the underlying provider
πŸ’‘ Protocol Abstraction: FeatureToggleService protocol hides all provider-specific details
πŸ’‘ Dependency Injection: Provider is injected via constructor - easy to swap
πŸ’‘ Zero Breaking Changes: Rest of your app continues working without recompilation

Total Implementation Time: ~2-4 hours for most providers (just 5 methods to implement!)

Supported Providers (community & potential):

  • βœ… Firebase Remote Config (included)
  • βœ… Mock Provider (included for testing)
  • πŸ”„ LaunchDarkly (easy to add)
  • πŸ”„ Split.io (easy to add)
  • πŸ”„ Optimizely (easy to add)
  • πŸ”„ Custom REST API (easy to add)
  • πŸ”„ Local UserDefaults (easy to add)

πŸŽ“ Why This Architecture?

This library demonstrates several software engineering principles:

  1. Dependency Inversion Principle (DIP): Feature modules depend on abstractions, not concrete implementations - enabling provider swapping
  2. Interface Segregation Principle (ISP): Separate interfaces for different responsibilities (value providing, listener management, etc.)
  3. Single Responsibility Principle (SRP): Each module has one clear purpose
  4. Open/Closed Principle: Open for extension (new providers) but closed for modification (existing code)
  5. Separation of Concerns: Clear boundaries between interface, implementation, and testing

Perfect for:

  • πŸ“± Large-scale iOS apps with multiple teams and modules
  • πŸ§ͺ Test-driven development (TDD) workflows
  • πŸš€ Continuous delivery with dynamic feature rollouts
  • πŸ‘₯ Team collaboration with clear module boundaries
  • 🎯 A/B testing and gradual feature rollouts

πŸ”„ Real-World Use Cases

// Gradual feature rollout
if featureToggleKit.getBoolValue(key: "new_checkout_flow") {
    // Show new checkout UI
} else {
    // Keep old checkout UI
}

// Kill switch for problematic features
if !featureToggleKit.getBoolValue(key: "enable_analytics") {
    return // Disable without app update
}

// Dynamic configuration
let apiTimeout = featureToggleKit.getLongValue(key: "api_timeout_seconds")
let apiConfig = featureToggleKit.getJSONValue(key: "api_config")

// A/B testing variants
let variant = featureToggleKit.getStringValue(key: "experiment_variant") // "A" or "B"

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

⭐ Show Your Support

If you find this library useful, please consider giving it a star! It helps others discover the project.

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ‘€ Author

Harry Nguyen Chi Hoang

πŸ™ Acknowledgments

About

🎯 Production-ready iOS feature toggle library with ZERO vendor lock-in. Swap Firebase for LaunchDarkly with ONE line change. Clean Architecture + SOLID principles. Swift 6 β€’ Modular β€’ 100% Testable

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors