Feature Flags в Swift: как включать фичи без нового билда и не сломать продакшн
Представьте: вы выкатили новую тёмную тему, а пользователи пишут, что текст стал нечитаемым. Или добавили сложный алгоритм рекомендаций, который на старых iPhone работает с лагами. В классическом сценарии вы правите код, собираете билд, проходите ревью, ждёте App Store — сутки, а то и больше. Всё это время баг живёт в продакшне.
Feature flags (или feature toggles) — это техника, которая позволяет включать и выключать функциональность приложения без деплоя нового кода. По сути, это условные операторы, которые проверяют значение флага, а значение хранится удалённо — на сервере или в Remote Config. Один переключатель — и фича либо появляется у всех, либо исчезает за секунды.
В этой статье мы разберём, как внедрить feature flags в Swift, какие паттерны использовать, и как не выстрелить себе в ногу. Поехали!
Зачем нужны feature flags в iOS-разработке
Многие думают, что фича-флаги — это только про A/B тесты. На самом деле сценариев гораздо больше:
- Канареечные релизы (canary releases) — включаете новую фичу для 5% пользователей, мониторите краши, и если всё ок — раскатываете на всех.
- Киллер-фичи (kill switches) — если что-то пошло не так, вы просто отключаете функционал удалённо, без отката билда.
- Поэтапный роллаут — показываете фичу сначала iOS 17+, потом более старым версиям.
- Платные фичи — скрываете премиум-функции за подпиской, управляя флагами на бэкенде.
- Эксперименты и A/B тесты — проверяете гипотезы на реальных пользователях, не создавая отдельные ветки.
Особенно это актуально для Swift-проектов, где бинарник собирается один раз, а потом лежит в сторе. Без фича-флагов вы не сможете быстро отреагировать на проблемы — только через новый релиз.
Архитектура feature flags в Swift
Простая реализация — это словарь [String: Bool], который приходит с сервера. Но в боевом проекте лучше сделать нормальную абстракцию. Вот ключевые компоненты:
- FeatureFlagProvider — источник данных (Remote Config, ваш API, локальный файл).
- FeatureFlagManager — синглтон или сервис, который кеширует флаги и отдаёт их вьюхам.
- FeatureFlagKey — enum с типизированными ключами, чтобы не размазывать строки по коду.
Давайте напишем простую, но рабочую реализацию на Swift.
Шаг 1. Определяем enum с ключами
enum FeatureFlagKey: String, CaseIterable {
case darkMode = "dark_mode"
case newOnboarding = "new_onboarding"
case premiumAnalytics = "premium_analytics"
case experimentalFeed = "experimental_feed"
}
Использование enum вместо строк гарантирует, что вы не опечатаетесь в названии флага. CaseIterable пригодится для дебажного экрана.
Шаг 2. Провайдер — получаем флаги из Firebase Remote Config
import FirebaseRemoteConfig
final class RemoteConfigProvider: FeatureFlagProvider {
private let remoteConfig = RemoteConfig.remoteConfig()
func fetchFlags(completion: @escaping ([String: Bool]) -> Void) {
remoteConfig.fetch(withExpirationDuration: 3600) { status, error in
if status == .success {
self.remoteConfig.activate { _, _ in
let flags = FeatureFlagKey.allCases.reduce(into: [String: Bool]()) { result, key in
result[key.rawValue] = self.remoteConfig.configValue(forKey: key.rawValue).boolValue
}
completion(flags)
}
} else {
print("Failed to fetch remote config: \(error?.localizedDescription ?? "")")
completion([:])
}
}
}
}
Здесь мы используем Firebase — стандартный выбор для iOS. Но можно подставить любой другой сервис: LaunchDarkly, Split, Unleash или свой бэкенд.
Шаг 3. Менеджер — единая точка доступа
final class FeatureFlagManager {
static let shared = FeatureFlagManager()
private var flags: [String: Bool] = [:]
private let provider: FeatureFlagProvider
private init(provider: FeatureFlagProvider = RemoteConfigProvider()) {
self.provider = provider
}
func loadFlags() {
provider.fetchFlags { [weak self] fetchedFlags in
self?.flags = fetchedFlags
}
}
func isEnabled(_ key: FeatureFlagKey) -> Bool {
return flags[key.rawValue] ?? false
}
}
Метод isEnabled возвращает дефолтное значение false, если флаг не найден. Это безопасно — новая фича не включится сама собой.
Шаг 4. Используем в коде
if FeatureFlagManager.shared.isEnabled(.darkMode) {
view.overrideUserInterfaceStyle = .dark
} else {
view.overrideUserInterfaceStyle = .light
}
Всё просто. Но это только начало. Настоящая мощь раскрывается, когда вы начинаете комбинировать флаги с аудиториями и процентами.
Продвинутые сценарии: проценты, аудитории и A/B тесты
Простое true/false — это база. Но в реальности вам может понадобиться включить фичу для 20% пользователей, или только для тех, кто купил подписку, или для жителей определённого региона.
Для этого флаги хранят не просто булево значение, а JSON с условиями. Пример структуры:
{
"dark_mode": {
"enabled": true,
"rollout_percentage": 50,
"target_audience": {
"min_app_version": "2.3.0",
"countries": ["RU", "BY"]
}
}
}
Обработка на клиенте:
func isDarkModeEnabled(for user: User) -> Bool {
guard let config = flags["dark_mode"] else { return false }
guard config.enabled else { return false }
guard config.rolloutPercentage > Double.random(in: 0...100) else { return false }
guard config.targetAudience.countries.contains(user.country) else { return false }
return true
}
Важно: случайное число генерируется один раз при старте сессии и сохраняется в UserDefaults, чтобы пользователь не прыгал между вариантами при каждом открытии экрана.
Безопасность и тестирование feature flags
Фича-флаги — мощный инструмент, но без дисциплины они превращают код в лапшу. Вот несколько правил, которые мы используем в продакшн-проектах:
1. Никаких вложенных флагов
Не проверяйте один флаг внутри другого. Это делает систему непредсказуемой. Если нужно — создайте отдельный флаг для комбинации.