swift

SwiftUI: onAppear и когда нужна ViewModel

Введение для новичков

SwiftUI — это современный фреймворк от Apple для создания интерфейсов.

Он декларативный: вы описываете, как должен выглядеть экран, а SwiftUI сам заботится об обновлениях.

Что значит, “SwiftUI сам заботится об обновлениях” ?

Дело в том, что данные на экране должны быть актуальными – представьте, что вы пишите приложение о прогнозе погоды, очевидно, что как только температура изменится – мы должны увидеть это изменения на экране телефона.

Раньше приходилось вручную отслеживать, что некие данные изменились и запускать их перерисовку на экране вручную – это приводило к тому, что какие-то данные мы забывали обновить и пользователь видел неактуальную информацию.

SwiftUI заботится о нас и сам обновляет данные – это позволяет писать меньше кода, а значит и уменьшить вероятность ошибки.

Давайте разбираться по порядку.

Требования для автоматического обновления

Чтобы UI обновлялся автоматически при изменении данных, нужны три обязательных условия:

  1. Класс наследник ObservableObject
  2. Свойства отмечены @Published
  3. ViewModel во View помечено как @StateObject или @ObservedObject (если это вспомогательное View и получает ViewModel от одного View)

Без любого из этих трёх пунктов — автоматических обновлений не будет.

Как работает автоматическое обновление с @Published

Требование всего одно: View должна подписаться на изменения ViewModel

Когда вы ставите @Published перед свойством в классе, который наследуется от ObservableObject, происходит следующее:

  1. Каждый раз при изменении этого свойства класс посылает сигнал “я изменился”
  2. 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:

“Я создаю этот объект, я им владею, и я отвечаю за его жизнь”

Получается, что если у вас есть Основное Вью – в нём мы и создадим наш @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 покажется на экране.

Как вы верно знаете – мало написать функцию, её нужно вовремя запустить.

Запуск функции, по большому счёту происходит из:

Простой пример для .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: правильный паттерн

Идеальное разделение ответственности:

Короткое правило для новичков

onAppear используйте как “будильник” — он говорит: “пора начинать загрузку”.

ViewModel нужна, когда в вашем onAppear или других частях view появляется больше 3-5 строк логики, особенно если эта логика работает с сетью, базой данных или сложными вычислениями.

Золотая середина:

Частые ошибки новичков

Ошибка 1: Загрузка данных в init вместо onAppear

init {
    loadData() // ❌ может вызваться раньше времени
}

init совсем не гарантирует, что View появилось на экране – он вызывается в момент создания модели. А создать модель мы можем когда угодно.

Правильно: грузим в onAppear, когда экран точно виден пользователю.

Ошибка 2: Забывают про состояние загрузки

.onAppear {
    viewModel.loadData() // нет индикатора загрузки для пользователя
}

Необходимо показать пользователю лоадер, который покажет пользователю, что приложение вовсе не зависло, а грузит данные. Т.к. это может занимать продолжительное время.

При этом, нужно учесть и состояние, при котором данные не загрузились.

Ошибка 3: Не используют @StateObject

let viewModel = MyViewModel() // ❌ не будет обновлять UI

Как только на экране есть поля, которые могут измениться – нам нужен @StateObject или @ObservedObject

@ObservedObject var viewModel = MyViewModel()

Теперь мы сможем автоматически обновить экран, как только поле изменится.