Compile-time feature flags keyed off the build configuration, shared through the SwiftUI environment. They support trunk-based development: merge unfinished features to main, ship them dark in the App Store build, and light them up in Debug and TestFlight.
Map configurations to a Distribution enum
Duplicate the Release configuration into AppStore and TestFlight, give each its own scheme and compilation condition, then resolve the active one once.
public enum Distribution: Sendable {
case debug
case appstore
case testflight
}
extension Distribution {
static var current: Self {
#if APPSTORE
return .appstore
#elseif TESTFLIGHT
return .testflight
#else
return .debug
#endif
}
}
Derive the flags per distribution
One struct owns every flag, initialized by switching on the distribution. Debug turns everything on. TestFlight disables the paywall so testers can exercise paid features. AppStore keeps unfinished work off.
public struct FeatureFlags: Sendable, Decodable {
public let requirePaywall: Bool
public let requireOnboarding: Bool
public let featureX: Bool
public init(distribution: Distribution) {
switch distribution {
case .debug:
(requirePaywall, requireOnboarding, featureX) = (true, true, true)
case .appstore:
(requirePaywall, requireOnboarding, featureX) = (true, true, false)
case .testflight:
(requirePaywall, requireOnboarding, featureX) = (false, true, true)
}
}
}
Share through the environment
Define an environment entry with the @Entry macro and inject the current flags at the root, so
any view can gate its behavior.
extension EnvironmentValues {
@Entry public var featureFlags = FeatureFlags(distribution: .debug)
}
@main
struct CardioBotApp: App {
var body: some Scene {
WindowGroup {
RootView()
.environment(\.featureFlags, FeatureFlags(distribution: .current))
}
}
}
Flags are temporary: delete each one once the feature has shipped and proven itself. If the project grows, move the values behind remote configuration so rollout and rollback are instant.