Реалізація 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 встановлюють default середовище для кожної конфігурації:
// 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 проект (аналітика, Remote Config, Crashlytics — різні проекти для dev/prod)
- Feature flags defaults
- Push notification середовище (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 з двома середовищами — 1 день. Повна інтеграція з різними Firebase проектами, APNs середовищами, DI-пересоздання мережевого шару — до трьох днів.







