SwiftUIJuniorTechnical
Что такое @Environment и какие встроенные environment values используются чаще всего?
@Environment(\.<key>) читает системные значения из окружения SwiftUI: colorScheme, locale, dismiss, openURL, scenePhase и др. Значения текут сверху вниз; переопределяются через .environment(\.key, value) на родителе. Только чтение — не изменяет environment.
Что такое @Environment
@Environment читает значение из environment SwiftUI по типобезопасному ключу (KeyPath). Значения текут сверху вниз по дереву view: родительский view может переопределить их через модификатор .environment(\.keyPath, value), и все потомки увидят новое значение, не получая его через параметры.
struct MyView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.locale) private var locale
@Environment(\.dynamicTypeSize) private var typeSize
var body: some View {
Text(colorScheme == .dark ? "Тёмная тема" : "Светлая тема")
}
}
Часто используемые встроенные EnvironmentValues
| Ключ | Тип | Когда нужен |
|---|---|---|
\.colorScheme | ColorScheme | Адаптация к тёмной/светлой теме |
\.locale | Locale | Форматирование дат, чисел, строк |
\.dynamicTypeSize | DynamicTypeSize | Адаптация layout под размер текста |
\.isEnabled | Bool | Проверка, доступен ли элемент для взаимодействия |
\.dismiss | DismissAction | Закрытие sheet / popover / NavigationStack без ссылки на родителя |
\.openURL | OpenURLAction | Открытие URL через системный обработчик |
\.scenePhase | ScenePhase | Реакция на active / inactive / background |
\.horizontalSizeClass | UserInterfaceSizeClass? | Адаптация layout под компактный / regular экран |
\.accessibilityReduceMotion | Bool | Отключение анимаций при настройке Reduce Motion |
\.font | Font? | Наследование шрифта из родителя |
Практический пример: dismiss + openURL + scenePhase
import SwiftUI
struct ProfileSheet: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.openURL) private var openURL
@Environment(\.colorScheme) private var scheme
let profileURL: URL
var body: some View {
VStack(spacing: 16) {
Text("Профиль")
.font(.title)
.foregroundStyle(scheme == .dark ? .white : .black)
Button("Открыть в браузере") {
openURL(profileURL) // используем Environment action
}
Button("Закрыть") {
dismiss() // закрываем sheet без @Binding
}
}
.padding()
}
}
Реакция на смену ScenePhase
struct AppRoot: View {
@Environment(\.scenePhase) private var scenePhase
var body: some View {
ContentView()
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .background:
// сохранить черновики, сбросить чувствительные данные
print("Приложение ушло в фон")
case .inactive:
print("Приложение неактивно")
case .active:
print("Приложение активно")
@unknown default:
break
}
}
}
}
Создание собственного EnvironmentKey
// 1. Определить ключ
struct AnalyticsClientKey: EnvironmentKey {
static let defaultValue: AnalyticsClient = .live
}
extension EnvironmentValues {
var analyticsClient: AnalyticsClient {
get { self[AnalyticsClientKey.self] }
set { self[AnalyticsClientKey.self] = newValue }
}
}
// 2. Внедрить в тестах или Preview
MyFeatureView()
.environment(\.analyticsClient, .mock)
// 3. Читать в view
struct MyFeatureView: View {
@Environment(\.analyticsClient) private var analytics
var body: some View {
Button("Купить") {
analytics.track("purchase_tapped")
}
}
}
Подводные камни
- @Environment — только чтение. Property wrapper читает значение, но не позволяет его изменить напрямую. Для записи используйте
.environment(\.key, value)на родительском view. - DismissAction работает только внутри sheet / fullScreenCover / NavigationStack. Вызов
dismiss()в корневом view ничего не делает и не даёт ошибки — баг сложно заметить. - scenePhase читается точно, если вешать onChange на App или WindowGroup. Если читать в глубоко вложенном view, значение может отражать фазу сцены, а не всего приложения.
- Не кешируйте Environment-значения вне body. Они обновляются при каждом render-проходе. Сохранение в локальную переменную вне body приведёт к устаревшим данным.
- Переопределение через .environment() влияет только на поддерево. Если случайно поставить модификатор не там — потомки не получат новое значение, а ошибки компилятора не будет.
- dynamicTypeSize не равен UIFont.preferredFont. SwiftUI управляет шрифтом через систему, но если вы вычисляете рамки вручную (GeometryReader + размеры), изменение DynamicTypeSize не пересчитает их автоматически.
- Нет runtime-ошибки при отсутствии внедрённого значения (в отличие от @EnvironmentObject): вернётся
defaultValue. Это может скрыть ошибку конфигурации — в тестах всегда внедряйте явно. - В iOS 17+ появился @Environment для @Observable-объектов. Синтаксис:
@Environment(MyModel.self)без KeyPath. Не путайте с KeyPath-вариантом — это разные API.
Common mistakes
- Сводить «
@Environmentи какие встроенные environment values используются чаще всего» к синтаксису и не объяснять идентичность view. - Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swiftui-5.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «
@Environmentи какие встроенные environment values используются чаще всего» и подтверждает ее корректным примером. - Умеет связать ответ с инвалидация body, тестированием и отладкой на устройстве.
- Называет ограничения подхода swiftui-5, включая производительность, память и сопровождение.