--- source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/feature-flags.mdc confidence: 0.9 namespace: work last_synced: 2026-04-28 alwaysApply: false --- # Feature Flag Patterns ## Type-Safe Feature Flags Use enum-based feature flags with protocols for type safety: ```swift // ✅ CORRECT - Type-safe feature flags protocol FeatureFlag: RawRepresentable where RawValue == String { var defaultValue: Bool { get } var description: String { get } } enum UIFeatureFlag: String, FeatureFlag { case newTabPageRedesign = "new_tab_page_redesign" case advancedPrivacySettings = "advanced_privacy_settings" case voiceSearch = "voice_search" case experimentalUI = "experimental_ui" var defaultValue: Bool { switch self { case .newTabPageRedesign: return false case .advancedPrivacySettings: return true case .voiceSearch: return false case .experimentalUI: return false } } var description: String { switch self { case .newTabPageRedesign: return "Enable redesigned new tab page" case .advancedPrivacySettings: return "Show advanced privacy settings" case .voiceSearch: return "Enable voice search functionality" case .experimentalUI: return "Enable experimental UI components" } } } enum NetworkFeatureFlag: String, FeatureFlag { case networkProtectionV2 = "network_protection_v2" case enhancedBlocking = "enhanced_blocking" var defaultValue: Bool { switch self { case .networkProtectionV2: return false case .enhancedBlocking: return true } } var description: String { switch self { case .networkProtectionV2: return "Enable Network Protection V2" case .enhancedBlocking: return "Enhanced content blocking" } } } ``` ## FeatureFlagger Protocol Implementation Extend the existing FeatureFlagger with type-safe methods: ```swift // ✅ CORRECT - Type-safe FeatureFlagger extension extension FeatureFlagger { func isEnabled(_ flag: Flag) -> Bool { return isFeatureOn(flag.rawValue) ?? flag.defaultValue } func setEnabled(_ enabled: Bool, for flag: Flag) { setFeatureOn(flag.rawValue, enabled: enabled) } } // Usage in code final class FeatureViewModel: ObservableObject { private let featureFlagger: FeatureFlagger init(featureFlagger: FeatureFlagger) { self.featureFlagger = featureFlagger } func loadContent() { if featureFlagger.isEnabled(UIFeatureFlag.newTabPageRedesign) { loadNewDesign() } else { loadLegacyDesign() } } } ``` ## Feature Flag ViewModifier Create SwiftUI modifiers for conditional UI: ```swift // ✅ ADVANCED - SwiftUI feature flag modifier struct FeatureFlagModifier: ViewModifier { let flag: Flag let featureFlagger: FeatureFlagger let fallback: () -> AnyView func body(content: Content) -> some View { if featureFlagger.isEnabled(flag) { content } else { fallback() } } } extension View { func featureFlag( _ flag: Flag, featureFlagger: FeatureFlagger, @ViewBuilder fallback: @escaping () -> some View = { EmptyView() } ) -> some View { modifier(FeatureFlagModifier( flag: flag, featureFlagger: featureFlagger, fallback: { AnyView(fallback()) } )) } } // Usage struct ContentView: View { @Environment(\.dependencies) var dependencies var body: some View { VStack { NewFeatureView() .featureFlag(UIFeatureFlag.experimentalUI, featureFlagger: dependencies.featureFlagger) { LegacyFeatureView() } } } } ``` ## Feature Flag Property Wrapper Create a property wrapper for reactive feature flags: ```swift // ✅ ADVANCED - Reactive feature flag property wrapper @propertyWrapper struct FeatureFlagState: DynamicProperty { @ObservedObject private var flagger: ObservableFeatureFlagger private let flag: Flag var wrappedValue: Bool { get { flagger.isEnabled(flag) } nonmutating set { flagger.setEnabled(newValue, for: flag) } } var projectedValue: Binding { Binding( get: { wrappedValue }, set: { wrappedValue = $0 } ) } init(_ flag: Flag) { self.flag = flag self._flagger = ObservedObject(wrappedValue: ObservableFeatureFlagger.shared) } } // Usage in SwiftUI views struct SettingsView: View { @FeatureFlagState(UIFeatureFlag.voiceSearch) var voiceSearchEnabled @FeatureFlagState(UIFeatureFlag.experimentalUI) var experimentalUIEnabled var body: some View { Form { Toggle("Voice Search", isOn: $voiceSearchEnabled) Toggle("Experimental UI", isOn: $experimentalUIEnabled) } } } ``` ## A/B Testing Integration Integrate feature flags with A/B testing: ```swift // ✅ ADVANCED - A/B testing with feature flags enum ABTestVariant: String, CaseIterable { case control = "control" case variantA = "variant_a" case variantB = "variant_b" var displayName: String { switch self { case .control: return "Control Group" case .variantA: return "Variant A" case .variantB: return "Variant B" } } } protocol ABTestFeatureFlag: FeatureFlag { var variants: [ABTestVariant] { get } var currentVariant: ABTestVariant { get } } enum ExperimentalFeatureFlag: String, ABTestFeatureFlag { case newOnboardingFlow = "new_onboarding_flow" case redesignedSearch = "redesigned_search" var defaultValue: Bool { true } var description: String { switch self { case .newOnboardingFlow: return "New onboarding flow experiment" case .redesignedSearch: return "Redesigned search interface experiment" } } var variants: [ABTestVariant] { [.control, .variantA, .variantB] } var currentVariant: ABTestVariant { // Get variant from A/B testing service return ABTestingService.shared.getVariant(for: self.rawValue) } } // Usage with variants func configureOnboarding() { let experiment = ExperimentalFeatureFlag.newOnboardingFlow switch experiment.currentVariant { case .control: showLegacyOnboarding() case .variantA: showNewOnboardingVariantA() case .variantB: showNewOnboardingVariantB() } // Track experiment exposure PixelFiring.fire(.experimentExposure(experiment.rawValue, experiment.currentVariant.rawValue)) } ``` ## Remote Feature Flags Integrate with remote configuration: ```swift // ✅ ADVANCED - Remote feature flag management protocol RemoteFeatureFlag: FeatureFlag { var remoteKey: String { get } var localOverrideKey: String? { get } } extension RemoteFeatureFlag { var remoteKey: String { rawValue } var localOverrideKey: String? { "local_override_\(rawValue)" } } enum RemoteUIFeatureFlag: String, RemoteFeatureFlag { case serverDrivenUI = "server_driven_ui" case dynamicThemes = "dynamic_themes" var defaultValue: Bool { false } var description: String { switch self { case .serverDrivenUI: return "Server-driven UI configuration" case .dynamicThemes: return "Dynamic theme system" } } } final class RemoteFeatureFlagger: FeatureFlagger { private let remoteConfig: RemoteConfigProtocol private let localDefaults: UserDefaults init(remoteConfig: RemoteConfigProtocol, localDefaults: UserDefaults) { self.remoteConfig = remoteConfig self.localDefaults = localDefaults } func isEnabled(_ flag: Flag) -> Bool { // Check local override first if let overrideKey = flag.localOverrideKey, let localOverride = localDefaults.object(forKey: overrideKey) as? Bool { return localOverride } // Check remote config if let remoteValue = remoteConfig.boolValue(for: flag.remoteKey) { return remoteValue } // Fall back to default return flag.defaultValue } func setLocalOverride(_ enabled: Bool?, for flag: Flag) { guard let overrideKey = flag.localOverrideKey else { return } if let enabled = enabled { localDefaults.set(enabled, forKey: overrideKey) } else { localDefaults.removeObject(forKey: overrideKey) } } } ``` ## Debug Feature Flag Interface Create debug interface for testing: ```swift // ✅ DEBUG - Feature flag debug interface #if DEBUG struct FeatureFlagDebugView: View { @StateObject private var debugFlags = DebugFeatureFlags() var body: some View { NavigationView { List { Section("UI Features") { ForEach(UIFeatureFlag.allCases, id: \.rawValue) { flag in FeatureFlagRow(flag: flag, debugFlags: debugFlags) } } Section("Network Features") { ForEach(NetworkFeatureFlag.allCases, id: \.rawValue) { flag in FeatureFlagRow(flag: flag, debugFlags: debugFlags) } } } .navigationTitle("Feature Flags") } } } struct FeatureFlagRow: View where Flag: CaseIterable { let flag: Flag @ObservedObject var debugFlags: DebugFeatureFlags var body: some View { Toggle(isOn: Binding( get: { debugFlags.isEnabled(flag) }, set: { debugFlags.setEnabled($0, for: flag) } )) { VStack(alignment: .leading) { Text(flag.rawValue) .font(.headline) Text(flag.description) .font(.caption) .foregroundColor(.secondary) } } } } #endif ``` See `analytics-patterns.md` for pixel firing patterns and `configuration-management.md` for advanced configuration management.