onAppear и когда нужна ViewModelSwiftUI — это современный фреймворк от Apple для создания интерфейсов.
Он декларативный: вы описываете, как должен выглядеть экран, а SwiftUI сам заботится об обновлениях.
Что значит, “SwiftUI сам заботится об обновлениях” ?
Дело в том, что данные на экране должны быть актуальными – представьте, что вы пишите приложение о прогнозе погоды, очевидно, что как только температура изменится – мы должны увидеть это изменения на экране телефона.
Раньше приходилось вручную отслеживать, что некие данные изменились и запускать их перерисовку на экране вручную – это приводило к тому, что какие-то данные мы забывали обновить и пользователь видел неактуальную информацию.
SwiftUI заботится о нас и сам обновляет данные – это позволяет писать меньше кода, а значит и уменьшить вероятность ошибки.
Давайте разбираться по порядку.
Чтобы UI обновлялся автоматически при изменении данных, нужны три обязательных условия:
ObservableObject@PublishedViewModel во View помечено как @StateObject или @ObservedObject (если это вспомогательное View и получает ViewModel от одного View)Без любого из этих трёх пунктов — автоматических обновлений не будет.
@PublishedТребование всего одно: View должна подписаться на изменения ViewModel
Когда вы ставите @Published перед свойством в классе, который наследуется от ObservableObject, происходит следующее:
View, которая подписана, автоматически перерисовываетсяКак View подписывается?
Ваша ViewModel должна быть подписана под протокол ObservableObject, если в ней есть хотя бы одно поле, которое может измениться на экране.
class TemperatureViewModel: ObservableObject {
/// Температура в данный момент
@Published var temperature: Int = 20
/// Поле не может измениться
let conditionerName = "Siemens"
/// Обновление температуры
func updateTemperature() {
temperature = 100
}
}
Представьте, что на экране мы показываем температуру воздуха и вдрух пользователь нажал кнопку “updateTemperature” – очевидно, что температура может оказаться другой и нам необходимо пометить поле temperature как @Published, что мы и сделали.
Но если поле никак не может измениться, за время нахождения пользователя на экране – нам нет смысла помечать это поле как @Published
Например у нас есть поле, которые показывает модель кондиционера и нет такой логики, которая позволит этому полю измениться – получается, что нам вовсе не зачем помечать его как @Published
@StateObject vs @ObservedObject: главное различиеЭто самая частая путаница у новичков. Различие всего в одном слове, но оно критическое.
@StateObject — Основное вью (Может быть только одно)Когда вы пишете @StateObject var viewModel = MyViewModel(), вы говорите SwiftUI:
“Я создаю этот объект, я им владею, и я отвечаю за его жизнь”
ViewModel остаётся той же самойSwiftUI сохраняет его между перерисовкамиПолучается, что если у вас есть Основное Вью – в нём мы и создадим наш @StateObject var viewModel = MyViewModel(), а вот во все вспомогательные View мы передадим уже созданную viewModel
Дело в том, что нам нужна только ОДНА ViewModel, а не куча независимых, иначе получится так, что каждое View будет работать со своей отдельной ViewModel и мы не сможем синхронизировать нашу работу между Экранами.
(Это сложно уже нет времени проще написать)
@ObservedObject — Вспомогательное ViewКогда вы пишете @ObservedObject var viewModel: MyViewModel, вы говорите:
“Мне дали этот объект откуда-то извне, я просто пользуюсь им”
@ObservedObject var viewModel: MyViewModel необходимо использовать на вспомогательных (дочерних) экранах, так что бы у нас была только ОДНА основная модель и все изменения были сосредоточены лишь в ней.
(Тут сложно – это нужно доп. объяснять –позже )
Используйте @StateObject когда вы создаёте ViewModel внутри этой же View:
/// Главное вью
struct BaseView: View {
/// Создадим ОДИН раз в нашей ОСНОВНОЙ View
@StateObject var viewModel = MyViewModel() // ✅
var body: some View {
/// Передадим нашу viewModel во все дочерние View
ChildView(viewModel: viewModel)
}
}
Используйте @ObservedObject когда вам передают ViewModel извне:
/// Вспомогательное Вью
struct ChildView: View {
/// Нам не нужно создавать модель –её нам передаст Основное Вью
@ObservedObject var viewModel: MyViewModel // ✅ пришло от родителя
var body: some View {
Text(viewModel.title)
}
}
// 1. ViewModel: ObservableObject + @Published
class CounterViewModel: ObservableObject {
@Published var count = 0
}
// 2. View: @StateObject (владелец)
struct CounterView: View {
@StateObject var viewModel = CounterViewModel()
var body: some View {
Button("Нажали \(viewModel.count) раз") {
viewModel.count += 1 // автоматически обновит UI
}
}
}
При каждом нажатии меняется @Published свойство → ViewModelпосылает сигнал → View перерисовывается.
В каждом xCode проекте есть функция, помеченная как @main именно с неё и начнётся работа нашего приложения.
@main
struct LearningApp: App {
var body: some Scene {
WindowGroup {
BeautySalonView()
}
}
}
В ней необходимо указать View, которое пользователь увидит первым. Зачастую это экран авторизации.
В конкретном случае – BeautySalonView станет нашей точной отсчёта.
Что бы запустить приложение и всё стало прям по серьёзке – нажмите command + r
onAppear?onAppear — это модификатор, который выполняет код в момент появления view на экране.
Представьте, что вам необходимо начать загрузку с сервера не раньше и не позже того момента как определённая View покажется на экране.
Как вы верно знаете – мало написать функцию, её нужно вовремя запустить.
Запуск функции, по большому счёту происходит из:
View появилось на экране (.onAppear)Button).onChange)Простой пример для .onAppear:
struct BeautySalonView: View {
// MARK: Properties
let viewModel = BeatySalonViewModel()
// MARK: Content
var body: some View {
BeautySalonView()
.onAppear {
/// Загрузим список рабочих, как только пользователь оказался на экране
viewModel.loadAdminList()
}
}
}
Какую проблему решает .onAppear ?
Представьте, что перед вами стоит задача показать список администраторов в салоне красоты, чтобы при начале смены конкретный администратор выбрал себя и начал работу.
Хорошо бы этот список начать загружать ровно в тот момент, как только появится View – без нажатия на кнопки и что бы мы не теряли время.
И такая возможность есть – система позаботилась об этом, благодаря .onAppear, которая есть в каждой View мы можем отслеживать появление текущего экрана перед глазами пользователя и начать выполнение функции сразу же!
ViewModel?ViewModel — это отдельный слой, который хранит состояние и логику экрана.
ViewModel:1. В вашей view появляется много полей, логики
@State private var isLoading = false
@State private var movies: [Movie] = []
@State private var errorMessage: String?
View должна быть максимально простой (без сложной логики), а наличие полей – может быть первым звоночком о том, что пора заводить ViewModel
Конечно, если это супер простой экран – ViewModel может и не понадобиться, не стоит заводить её всегда на автомате.
Если ваша View просто показывает уже готовые данные – скорей всего ViewModel будет излишней.
Но если данные, прежде чем их показать нужно ещё и загрузить от куда-то или подготовить правильным образом – без ViewModel не обойтись.
2. Во View слишком много логики, функций
.onAppear {
isLoading = true
loadData { result in
switch result {
case .success(let data):
movies = data
case .failure(let error):
errorMessage = error.localizedDescription
}
isLoading = false
}
}
Такой код сложно тестировать и переиспользовать.
В данном случае так и напрашивается добавить ViewModel и убрать всю логику из onAppear во ViewModel
3. Одна и та же логика нужна на нескольких экранах
Например, загрузка профиля пользователя или проверка авторизации.
Очень важно понимать, что функции должны быть переиспользуемыми, если вы напишите их во View – то вам не удастся использовать логику функции в другом месте, на другом экране.
Вынос логики в отдельные классы, позволяет в будущем переиспользовать логику функции, сколь угодно раз.
Представьте, что у нас есть функция, которая загружает список рабочих для нашего салона красоты, очевидно, что может быть уйма экранов, для которых нам потребуется подобная логика (список рабочих)
В таком случае нам будет удобно создать отдельный класс, который занимается тем, что загружает из сети json и декодирует из него наш список рабочих и этот класс использовать на всех экранах, не дублируя логику.
ViewModel на примере списка фильмов:class MoviesViewModel: ObservableObject {
@Published var movies: [Movie] = []
@Published var isLoading = false
@Published var errorMessage: String?
func loadMovies() {
isLoading = true
// загрузка данных...
}
}
struct MoviesView: View {
@StateObject private var viewModel = MoviesViewModel()
var body: some View {
List(viewModel.movies) { movie in
Text(movie.title)
}
.onAppear {
viewModel.loadMovies()
}
}
}
onAppear + ViewModel: правильный паттернИдеальное разделение ответственности:
View — отвечает только за отображение и передачу действий пользователяViewModel — содержит всю бизнес-логику, загрузку данных, обработку ошибокonAppear — вызывает функцию во ViewModel для старта загрузки данныхonAppear используйте как “будильник” — он говорит: “пора начинать загрузку”.
ViewModel нужна, когда в вашем onAppear или других частях view появляется больше 3-5 строк логики, особенно если эта логика работает с сетью, базой данных или сложными вычислениями.
Золотая середина:
ViewModelViewModelViewModelОшибка 1: Загрузка данных в init вместо onAppear
init {
loadData() // ❌ может вызваться раньше времени
}
init совсем не гарантирует, что View появилось на экране – он вызывается в момент создания модели. А создать модель мы можем когда угодно.
Правильно: грузим в onAppear, когда экран точно виден пользователю.
Ошибка 2: Забывают про состояние загрузки
.onAppear {
viewModel.loadData() // нет индикатора загрузки для пользователя
}
Необходимо показать пользователю лоадер, который покажет пользователю, что приложение вовсе не зависло, а грузит данные. Т.к. это может занимать продолжительное время.
При этом, нужно учесть и состояние, при котором данные не загрузились.
Ошибка 3: Не используют @StateObject
let viewModel = MyViewModel() // ❌ не будет обновлять UI
Как только на экране есть поля, которые могут измениться – нам нужен @StateObject или @ObservedObject
@ObservedObject var viewModel = MyViewModel()
Теперь мы сможем автоматически обновить экран, как только поле изменится.