Реализация Environment Switcher (переключение Dev/Staging/Prod) в мобильном приложении
Разработчик тестирует фичу против dev-сервера, QA-инженер проверяет на staging перед релизом, поддержка воспроизводит баг пользователя на prod-данных — и всё это без пересборки приложения. Environment Switcher — не DevOps-инструмент, это дисциплина мобильной разработки.
Почему hardcoded URL — это технический долг
Когда base URL захардкожен в Constants.swift или BuildConfig, каждое переключение среды требует изменения кода и пересборки. CI собирает три разных артефакта. Тестировщик ждёт новую сборку, чтобы проверить одно и то же на staging. Это не масштабируется.
Правильная архитектура
Уровень 1: Build-time конфигурация. Разные .xcconfig для iOS или buildConfigField для Android задают дефолтную среду для каждой конфигурации:
// build.gradle
buildTypes {
debug {
buildConfigField "String", "DEFAULT_ENV", "\"dev\""
buildConfigField "String", "API_URL_DEV", "\"https://api-dev.example.com\""
buildConfigField "String", "API_URL_STAGING", "\"https://api-staging.example.com\""
buildConfigField "String", "API_URL_PROD", "\"https://api.example.com\""
}
release {
buildConfigField "String", "DEFAULT_ENV", "\"prod\""
// staging и dev URL не нужны в release
}
}
Уровень 2: Runtime-переключение. Для debug/beta-сборок — сохраняем текущую среду в SharedPreferences/UserDefaults, читаем при каждом запросе:
enum Environment: String, CaseIterable {
case dev = "dev"
case staging = "staging"
case production = "production"
var baseURL: URL {
switch self {
case .dev: return URL(string: "https://api-dev.example.com")!
case .staging: return URL(string: "https://api-staging.example.com")!
case .production: return URL(string: "https://api.example.com")!
}
}
}
final class EnvironmentManager {
static let shared = EnvironmentManager()
var current: Environment {
get {
let raw = UserDefaults.standard.string(forKey: "app_environment")
?? Environment.dev.rawValue
return Environment(rawValue: raw) ?? .dev
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: "app_environment")
// сигнал для перезапуска сетевого слоя
NotificationCenter.default.post(name: .environmentDidChange, object: nil)
}
}
}
Уровень 3: Перезапуск сетевого слоя. После переключения среды нужно: разлогинить пользователя (токены dev-среды не работают на prod), очистить кеш, пересоздать URLSession / OkHttpClient с новым base URL. Если сетевой слой создан как синглтон с закешированным base URL — это не сработает без пересоздания. Правильная архитектура: NetworkClient принимает Environment через DI-контейнер, при смене среды пересоздаётся.
Что меняется вместе со средой
Среда — это не только URL. Полный список параметров, которые должны переключаться:
- API base URL
- WebSocket URL (если есть)
- Firebase project (аналитика, Remote Config, Crashlytics — разные проекты для dev/prod)
- Feature flags defaults
- Push notification environment (APNs sandbox vs production)
- Analytics tracking ID
Firebase для разных сред — через разные GoogleService-Info.plist (iOS) или google-services.json (Android). Выбор файла в зависимости от build flavor/configuration — стандартная практика.
Environment Switcher в UI
Размещается в Debug Menu (скрытый от пользователя) или в Settings для TestFlight/Beta-сборок:
Environment
● Development https://api-dev.example.com
○ Staging https://api-staging.example.com
○ Production https://api.example.com
[Switch] ← разлогинивает и применяет новую среду
После переключения — alert с предупреждением о разлогинивании и необходимости перезапуска. Можно сделать через exit(0) — спорно, но распространено в dev-сборках.
Защита от случайного использования в production
В release-сборке весь код Environment Switcher должен отсутствовать: условная компиляция #if DEBUG / debug build flavor. В production приложении нет смысла иметь URL staging или dev-сервера в бинарнике — это информация, которую не стоит раскрывать через reverse engineering.
Сроки — 1–3 дня. Простой switcher с двумя средами — день. Полная интеграция с разными Firebase проектами, APNs environments, DI-пересборкой сетевого слоя — до трёх дней.







