@StateObject — это Property Wrapper в SwiftUI для создания и владения экземпляром ObservableObject непосредственно во view. SwiftUI гарантирует, что объект инициализируется один раз за жизненный цикл представления и не пересоздаётся при повторных рендерах. По данным Apple Developer Documentation (2025), @StateObject рекомендуется для корневых view, создающих источник данных. @StateObject — правильный выбор для владения ObservableObject в иерархии SwiftUI.
Главное
@StateObject — это Property Wrapper, появившийся в SwiftUI 2.0 (iOS 14), который объединяет возможности @ObservedObject и @State. Как и @ObservedObject, он подписывается на изменения ObservableObject. Как @State, он гарантирует, что данные переживают повторные инициализации структуры view. @StateObject создаёт объект один раз при первом появлении view на экране и хранит его в куче SwiftUI.
До появления @StateObject разработчики использовали @ObservedObject для всех ObservableObject, включая создаваемые во view. Это приводило к частой потере данных при обновлении родительского view, когда структура view пересоздавалась, забирая с собой и @ObservedObject-экземпляр. @StateObject решил эту проблему, добавив гарантию стабильности.
Основное правило: @StateObject применяется в том view, которое создаёт объект в инициализаторе по умолчанию (let model = ViewModel()). Дочерние view, получающие этот объект, используют @ObservedObject. Такое разделение гарантирует единственный источник истины во всей иерархии.
SwiftUI управляет жизненным циклом @StateObject через storage-менеджер, аналогичный @State. При первом появлении view SwiftUI выделяет память для объекта и сохраняет его в персистентной области. При повторных рендерах (вызов body) объект не пересоздаётся — используется существующий экземпляр. Объект живёт, пока view находится в иерархии.
Когда view удаляется из иерархии, SwiftUI уничтожает @StateObject, вызывая deinit. При повторном добавлении view в иерархию создаётся новый экземпляр. Это важно учитывать при проектировании: если нужно сохранить данные между удалениями view, используйте сервисный слой (singleton или DI) или @AppStorage для персистентности.
class TimerViewModel: ObservableObject {
@Published var seconds: Int = 0
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
}
deinit {
timer?.invalidate()
}
}
struct TimerView: View {
@StateObject var viewModel = TimerViewModel()
var body: some View {
Text("\(viewModel.seconds)s")
.onAppear { viewModel.start() }
}
}
В примере TimerViewModel создаётся через @StateObject и живёт, пока TimerView на экране. Timer запускается в onAppear и останавливается в deinit. Если бы использовался @ObservedObject, при каждом рендере TimerView создавался бы новый TimerViewModel с секундами = 0, и таймер никогда бы не работал корректно. @StateObject гарантирует, что viewModel единственна и стабильна.
Выбор между @StateObject и @ObservedObject зависит от того, кто владеет объектом. Если view создаёт объект — @StateObject. Если view получает готовый объект — @ObservedObject. Это правило настолько важно, что Xcode выдаёт предупреждение (warning) при использовании @StateObject в дочернем view, получающем объект через инициализатор.
| Ситуация | Рекомендуемый wrapper |
|---|---|
| View создаёт модель через ViewModel() | @StateObject |
| View получает модель от родителя | @ObservedObject |
| Модель используется в одном view | @StateObject |
| Модель передаётся через Environment | @EnvironmentObject |
| Модель нужна для превью | @ObservedObject + mock |
На практике при старте проекта часто используют @StateObject в корневом view и @ObservedObject во всех дочерних. По мере роста приложения часть @StateObject может быть заменена на @EnvironmentObject для упрощения иерархии. Однако @StateObject остаётся лучшим выбором для модульных экранов с собственной логикой.
Первый паттерн — MVVM с @StateObject. ViewModel как ObservableObject создаётся во view через @StateObject. ViewModel содержит @Published-свойства и бизнес-логику. View подписывается на изменения и обновляет интерфейс. Такой подход даёт тестируемую изоляцию: ViewModel можно протестировать без UI, создавая экземпляр напрямую.
Второй паттерн — @StateObject с зависимостями. Если ViewModel требует сервисы, используйте инициализацию с параметрами. Например, @StateObject var viewModel = UserViewModel(api: APIClient.shared). Однако будьте осторожны: параметры вычисляются при каждом рендере body, но объект создаётся только один раз. SwiftUI игнорирует последующие инициализации @StateObject.
Третий паттерн — вложенные @StateObject. В SwiftUI можно иметь несколько @StateObject в одном view, но это редко оправдано. Обычно один @StateObject отвечает за весь набор данных view. Если логика становится слишком сложной, разбейте её на композицию @ObservedObject сервисов внутри одного @StateObject.
struct AppView: View {
@StateObject var router = NavigationRouter()
@StateObject var auth = AuthViewModel()
var body: some View {
ContentView()
.environmentObject(router)
.environmentObject(auth)
}
}
В примере AppView создаёт два @StateObject: NavigationRouter для управления навигацией и AuthViewModel для аутентификации. Оба объекта внедряются в Environment через environmentObject. Любое дочернее view может получить к ним доступ через @EnvironmentObject без передачи через цепочку инициализаторов.
@StateObject поддерживает инициализацию с любыми параметрами, но с важной особенностью: инициализатор вызывается только один раз. При повторных рендерах body новое значение в параметрах игнорируется. Это означает, что если вы передаёте @State var id: Int = 5 в @StateObject var vm = ViewModel(id: id), то при изменении id ViewModel не получит новое значение.
Для решения этой проблемы используйте onReceive или onAppear для синхронизации. Подпишитесь на изменения параметра внутри ViewModel через Combine или передавайте параметры через метод .onChange(of:) на уровне view. Альтернатива — использовать @ObservedObject вместо @StateObject, если объект должен динамически реагировать на внешние изменения.
struct DetailView: View {
let itemId: Int
@StateObject var viewModel = DetailViewModel()
var body: some View {
Text(viewModel.title)
.onAppear { viewModel.load(id: itemId) }
}
}
Правильный подход: DetailView получает itemId как let-свойство (передаётся через инициализатор структуры), а @StateObject создаёт DetailViewModel без параметров. В onAppear вызывается метод load(id:), который загружает данные для переданного ID. Это гарантирует, что ViewModel создана @StateObject-механизмом, но данные загружаются при каждом появлении view с актуальным ID.
Главная ошибка — использование @StateObject в дочерних view, которые получают объект от родителя. Если ParentView создаёт @StateObject model, а ChildView объявляет @StateObject var model: ModelType (с параметром по умолчанию), то ChildView создаст собственный независимый экземпляр. Родительский и дочерний объекты не будут связаны, и изменения в одном не отразятся на другом.
Вторая ошибка — помещение @StateObject в List или ForEach. Каждый элемент списка создаёт свой @StateObject, что ведёт к множеству независимых экземпляров. Для списков правильно передавать один ObservableObject всем элементам через @ObservedObject или использовать Identifiable структуры с @State внутри List.
Третья проблема — отсутствие deinit-очистки. @StateObject живёт весь lifecycle view. Если объект создаёт таймеры, подписки Combine или сетевые запросы, deinit должен их отменять. Иначе утечки памяти и продолжение фоновой работы после закрытия экрана неизбежны. Всегда используйте Combine Cancellable store или invalidate таймеров в deinit.
Часто задаваемые вопросы
@StateObject добавлен в SwiftUI 2.0 на WWDC 2020 вместе с iOS 14, macOS 11, watchOS 7 и tvOS 14. До этого @ObservedObject был единственным способом работы с ObservableObject, что приводило к частым багам с потерей данных.
Нет, @StateObject не поддерживает Optional-типы. Объект должен быть инициализирован при объявлении. Если нужен опциональный объект, используйте @ObservedObject или @EnvironmentObject с опциональным типом.
Добавьте print(#function) в инициализатор и deinit ObservableObject. Если при повторных рендерах init не вызывается — @StateObject работает корректно. Если init вызывается каждый раз — замените @ObservedObject на @StateObject.
Да, @StateObject работает в SwiftUI view, встроенных в UIKit через UIHostingController. Жизненный цикл объекта привязан к SwiftUI view, а не к UIViewController. Если SwiftUI view заменяется, @StateObject уничтожается.
Несколько маленьких @StateObject с разделённой ответственностью. Это улучшает testability, переиспользование и производительность — при изменении одного объекта перерисовываются только подписанные части интерфейса, а не всё view целиком.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также