为什么要"特性开关"
- 主干开发(trunk-based)要求频繁合入半成品代码,但又不希望用户踩雷。
- 同一套代码要在 Debug(调试)、TestFlight(内测)、App Store(正式)三种环境跑,功能粒度不同:
- Debug:全开,方便一口气测到底。
- TestFlight:想让人免费体验付费模块,好收集崩溃与性能数据。
- App Store:只敢把 100% 完成且审核通过的功能放出来。
 
- "特性开关"(Feature Flag)就是把这些"开/关"从 hard-code 的 if-else 里解放出来,做到编译期可配、运行时可达、后期可远程化。
核心思路三步走
- 用 Xcode Configuration + Swift 编译条件,让编译器帮你"裁剪"代码。
- 把开关做成一个值类型(struct/enum),一次性注入 SwiftUI 环境。
- 视图层只关心"开关是否打开",不关心"当前是什么环境"。
动手实战
- 
准备三套 Configuration 复制 Release → 分别取名 TestFlight、AppStore;  在 Build Settings → Swift Compiler - Custom Flags → Active Compilation Conditions 里加上: - Debug 空着(或 DEBUG)
- TestFlight 加 TESTFLIGHT
- AppStore 加 APPSTORE
  
- 
定义"当前渠道"枚举 
            
            
              swift
              
              
            
          
          /// 当前 App 的发布渠道
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
    }
}- 定义"特性开关"包
            
            
              swift
              
              
            
          
          /// 把所有可开关项集中在一处,方便 Review & 清理
public struct FeatureFlags: Sendable, Decodable {
    public let requirePaywall: Bool      // 是否强制付费墙
    public let requireOnboarding: Bool   // 是否显示新手引导
    public let featureX: Bool            // 某个尚未发布的新功能
    
    /// 根据渠道给出默认策略
    init(distribution: Distribution) {
        switch distribution {
        case .debug:               // 调试:全开,跑通再说
            self.requirePaywall = true
            self.requireOnboarding = true
            self.featureX = true
        case .appstore:            // 正式:保守,只开已审核模块
            self.requirePaywall = true
            self.requireOnboarding = true
            self.featureX = false
        case .testflight:          // 内测:给足权限,关闭付费墙
            self.requirePaywall = false
            self.requireOnboarding = true
            self.featureX = true
        }
    }
}- 注入 SwiftUI 环境
            
            
              swift
              
              
            
          
          extension EnvironmentValues {
    /// 全局可读的特性开关
    @Entry public var featureFlags = FeatureFlags(distribution: .debug)
}- 在 App 入口统一装配
            
            
              swift
              
              
            
          
          @main
struct CardioBotApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
                // 用 .current 让编译器自动选对配置
                .environment(\.featureFlags, FeatureFlags(distribution: .current))
        }
    }
}- 视图层使用示例
            
            
              swift
              
              
            
          
          struct PaywallView: View {
    @Environment(\.featureFlags) private var flags
    
    var body: some View {
        if flags.requirePaywall {
            // 真正的付费墙
            Text("真正的付费墙")
        } else {
            // TestFlight 直接送
            Text("感谢内测,已解锁 Pro 功能")
        }
    }
}- 
快速本地调试小技巧 在 Xcode Scheme → Run → Arguments → Environment Variables 里加键值 OVERRIDE_PAYWALL = 0/1,然后在 FeatureFlags.init里优先读ProcessInfo,就能在不换 Configuration 的情况下临时翻转开关,调 UI 更爽。
完整可拷贝的"迷你框架"
            
            
              swift
              
              
            
          
          // 1. 渠道
public enum Distribution: Sendable {
    case debug, appstore, testflight
    public static var current: Self {
#if APPSTORE
        return .appstore
#elseif TESTFLIGHT
        return .testflight
#else
        return .debug
#endif
    }
}
// 2. 开关
public struct FeatureFlags: Sendable {
    public let requirePaywall: Bool
    public let requireOnboarding: Bool
    public let featureX: Bool
    
    public init(distribution: Distribution = .current) {
        self.requirePaywall     = (distribution != .testflight)
        self.requireOnboarding  = true
        self.featureX           = (distribution != .appstore)
    }
}
// 3. 环境
extension EnvironmentValues {
    @Entry public var featureFlags = FeatureFlags(distribution: .debug)
}容易踩的坑
- 
编译条件大小写敏感,#if APPSTORE 与 "AppStore" 不是一回事。 
- 
忘了在新增 Configuration 里加 Flag,结果跑到 AppStore 分支却开了 Debug 开关。 
- 
开关泛滥:半年不清理,代码里满屏 if flags.xx把可读性拖垮。→ 建议给每个开关建 Issue,上线后第二周就安排清理。 
- 
开关被逆向:Bool 值直接埋包,越狱用户一翻就能改。 → 若业务敏感,后期应迁移到服务端签名下发。 
后续可扩展场景
- 
远程云控开关 把 FeatureFlags做成 Codable,App 启动时拉 JSON,再结合 Certificate Pinning + 签名验证,实现"秒级"回滚。
- 
A/B 实验 在远程 JSON 里加 bucket字段,按用户 ID 哈希到不同桶,同一个版本跑多套策略。
- 
模块级别懒加载 与 SwiftUI LazyView或 TCA 的Reducer组合,开关关闭时连模块都不装入内存,减少启动时间。
- 
框架多租户 为不同地区/渠道/品牌生成多套 FeatureFlags,但共用一套 Core,适合 SDK 厂商。
个人小结
特性开关不是"高大上"的专利,而是"三环境"工程里的最小可用实践。
先用编译条件解决 80% 问题,再逐步过渡到远程+灰度,既不会一上来就"过度设计",又保留了演进的余量。
把开关当"临时脚手架"看,上线第二版就拆,才能避免"flag 债"。
愿我们都能在 trunk-based 的快车道上,合并得早、发布得稳、回滚得秒。