Every serious app needs the ability to:
- turn features on/off remotely
- ship incomplete work safely
- run A/B tests
- kill broken features instantly
- stage rollouts gradually
If your answer is:
“We just use if statements”
…you are one production incident away from pain.
This post shows how to design clean, testable, scalable feature flag architecture in SwiftUI — without polluting your UI or ViewModels.
🧠 The Core Principle
Feature flags are configuration, not logic.
Your app should react to flags, not be built out of flags.
🧱 1. Define Feature Flags as a Typed Model
Never use raw strings:
if flags["new_home"] == true { }
Instead:
enum FeatureFlag: String, CaseIterable {
case newHome
case profileRedesign
case aiSearch
case experimentalCharts
}
Type safety matters.
📦 2. Feature Flag Service
protocol FeatureFlagService {
func isEnabled(_ flag: FeatureFlag) -> Bool
func refresh() async
}
Concrete implementation:
final class FeatureFlagServiceImpl: FeatureFlagService {
private var flags: [FeatureFlag: Bool] = [:]
func isEnabled(_ flag: FeatureFlag) -> Bool {
flags[flag] ?? false
}
func refresh() async {
let remote = try? await fetchRemoteFlags()
flags = remote ?? flags
}
}
🌐 3. Remote Config Layer
Remote config should:
- fetch on launch
- cache locally
- refresh in background
- fail gracefully
func fetchRemoteFlags() async throws -> [FeatureFlag: Bool] {
// API call here
}
Never block app launch.
💾 4. Local Fallback & Persistence
Always persist last-known flags:
func persist(_ flags: [FeatureFlag: Bool]) {
let data = try? JSONEncoder().encode(flags)
UserDefaults.standard.set(data, forKey: "feature_flags")
}
On launch:
func loadPersisted() -> [FeatureFlag: Bool] {
guard
let data = UserDefaults.standard.data(forKey: "feature_flags"),
let flags = try? JSONDecoder().decode([FeatureFlag: Bool].self, from: data)
else {
return [:]
}
return flags
}
This gives:
- instant startup
- offline safety
- consistent UX
🧩 5. Inject Flags into the Dependency Graph
Feature flags belong in AppContainer:
final class AppContainer {
let featureFlags: FeatureFlagService
// ...
}
Inject into environment:
.environment(\.featureFlags, appContainer.featureFlags)
Views consume — they never create.
🎯 6. How Views Should Use Flags
Bad:
if featureFlags.isEnabled(.newHome) {
NewHomeView()
} else {
OldHomeView()
}
Good:
HomeContainer()
Where HomeContainer resolves internally:
struct HomeContainer: View {
@Environment(\.featureFlags) var flags
var body: some View {
if flags.isEnabled(.newHome) {
NewHomeView()
} else {
OldHomeView()
}
}
}
Flags are isolated at boundaries, not everywhere.
🧠 7. ViewModel Usage
ViewModels should:
- receive flags via init
- expose derived behavior
final class HomeViewModel {
let showAI: Bool
init(flags: FeatureFlagService) {
self.showAI = flags.isEnabled(.aiSearch)
}
}
No flag lookups inside business logic.
🧪 8. Testing Becomes Trivial
struct MockFeatureFlags: FeatureFlagService {
let enabled: Set<FeatureFlag>
func isEnabled(_ flag: FeatureFlag) -> Bool {
enabled.contains(flag)
}
func refresh() async { }
}
let flags = MockFeatureFlags(enabled: [.newHome])
let vm = HomeViewModel(flags: flags)
No hacks. No globals.
🔁 9. Kill Switches (Critical)
Every risky feature must be:
- behind a flag
- default off
- remotely controllable
This allows:
- instant rollback
- no App Store resubmission
- no panic deploys
This is not optional in production.
⚠️ 10. Flag Lifecycle Management
Flags should have:
- owners
- descriptions
- removal plans
Dead flags are technical debt.
Track:
- creation date
- rollout status
- removal date
❌ 11. Common Anti-Patterns
Avoid:
- flags inside low-level components
- flags inside models
- flags in networking layer
- stringly-typed flags
- feature flags as permanent code paths
- flags controlling business rules
Flags control presentation & availability, not logic correctness.
🧠 Mental Model
Think of feature flags as:
Configuration Layer
↓
Feature Selection
↓
Normal App Flow
They are gates, not forks.
🚀 Final Thoughts
A proper feature flag system gives you:
- safer releases
- faster iteration
- controlled experiments
- instant rollback
- happier product teams
- calmer developers
Once you build this, you’ll never ship “big bang” features again.
Top comments (0)