Следующая статья поможет вам: Объяснение жизненного цикла приложения SwiftUI
Вернуться в блог
Прощай, AppDelegate! Теперь вы можете создавать приложения SwiftUI с новым протоколом и жизненным циклом приложения без необходимости делегата приложения или сцены. Как работает жизненный цикл приложения SwiftUI? И как его настроить? Давай выясним!
В этом руководстве по разработке приложений мы обсудим:
- Как работает жизненный цикл приложения SwiftUI и структура вашего приложения
- Работа с @main, сценами и событиями жизненного цикла.
- Загрузка пользовательского интерфейса вашего приложения с помощью протокола приложения
- Работа с UIApplicationDelegateAdaptor «по-старому»
Прежде чем мы перейдем к структуре App, давайте немного вернемся назад.
У каждого приложения и компьютерной программы есть отправная точка. Думайте об этом как о первой функции вашего приложения, вызываемой операционной системой (например, iOS). На большинстве платформ эта функция называется main().
До iOS 14 большинство приложений iOS имели класс AppDelegate (и, возможно, SceneDelegate) в своем проекте Xcode. Этот делегат приложения помечен ключевым словом @UIApplicationMain, указывающим, что этот делегат приложения является отправной точкой приложения.
Технически iOS создает экземпляр UIApplication и назначает экземпляр класса делегата вашего приложения его свойству делегата. Благодаря делегированию теперь вы можете настраивать то, что происходит при запуске приложения, подключаться к событиям жизненного цикла и настраивать «маршруты» для запуска вашего приложения, например пользовательские URL-адреса и локальные/удаленные уведомления.
Самая важная функция делегата приложения — application(_:didFinishLaunchingWithOptions:). Как ни странно, эта функция почти всегда возвращает true. Это «основная» функция приложения iOS, первая вызываемая за время существования приложения.
Обычно вы используете его для:
- Первоначальная настройка сторонних компонентов и платформ, например настройка Firebase, Bugsnag, Realm, OneSignal, Parse Server, служб Reachability и т. д.
- Если вы не используете делегат сцены, вы должны настроить здесь контроллер корневого представления окна приложения — «первый» пользовательский интерфейс приложения (в противном случае вы бы сделали это в делегате сцены или Info.plist).
- Изменение глобального внешнего вида вашего приложения с помощью прокси внешнего вида
- Реагирование на входящие удаленные или локальные уведомления, то есть приходит push-уведомление, и вы открываете определенный пользовательский интерфейс в приложении, а не обычный/основной пользовательский интерфейс.
Делегат приложения также используется для подключения к событиям жизненного цикла. Именно это подразумевает слово «жизненный цикл»: приложение запускается, становится активным, пользователь выполняет какое-то действие, а через некоторое время приложение сворачивается, переходит в фоновый режим и в конечном итоге закрывается.
В зависимости от вашего приложения жизненные циклы вообще не важны или очень важны. Например, игра для iOS должна будет приостановиться, когда вам позвонят, и приложение на некоторое время станет неактивным, пока вы не возобновите игру. Ресурсоемким приложениям потребуется приостанавливать таймеры и/или фоновые процессы или, по сути, запускать фоновый процесс при закрытии приложения. Все это происходит в App Delegate!
Но… как насчет жизненного цикла приложения SwiftUI?
В iOS 14 появился протокол приложений, который многие называют «SwiftUI 2.0». По сути, протокол приложения заменяет делегат приложения и берет на себя многие его функции. Ваше приложение теперь является «чистым» приложением SwiftUI; у него больше нет этого делегата приложения.
Вот как это выглядит для простого приложения:
@основной
struct BooksApp: Приложение
{
var body: некоторая сцена {
ОкноГруппа {
Список книг()
}
}
}
Потрясающий! Довольно лаконично, не так ли? Приведенный выше код загрузит все ваше приложение, настроив для него начальное представление под названием BookList.
Вот что происходит:
- Атрибут @main, объявленный для структуры BooksApp, указывает, что эта структура «содержит точку входа верхнего уровня для выполнения программы». Это причудливый способ сказать, что протокол приложения имеет реализацию по умолчанию для статической функции main(), которая инициализирует приложение при вызове.
- Код struct BooksApp: App { объявляет структуру BooksApp, которая использует протокол App. Вы можете назвать эту структуру как угодно, но обычно выбирают имя вашего приложения плюс «~App», например BooksApp.
- Тело var: некоторый код Scene { объявляет необходимое свойство body для протокола приложения, которое невероятно похоже на свойство body представления SwiftUI. Его тип — Scene (не View!), и благодаря ключевому слову some конкретный тип тела зависит от его реализации. Это шаблонный код; просто учтите, что «у приложения есть тело, которое предоставляет (первую) сцену для приложения».
- Внутри свойства body находится экземпляр WindowGroup. Это кроссплатформенная структура, представляющая сцену из нескольких windows. Вы можете использовать его в macOS, iOS и т. д. Это контейнер для иерархии представлений ваших приложений, что-то вроде старого доброго UIWindow.
- В WindowGroup вы объявляете первое представление («Пользовательский интерфейс») для своего приложения. В приведенном выше коде это представление BookList, но оно, конечно, может быть чем угодно.
Вы не можете не заметить сходство между протоколом SwiftUI View и протоколом приложения. Работа с приложением SwiftUI кажется знакомой!
Фактически, вы, возможно, заметили, что предпочтение UIKit к делегированию было заменено предпочтением SwiftUI в отношении компонуемости, протоколов и реализаций протоколов по умолчанию. Это то же самое, но другое.
Как начать работу с приложением SwiftUI? Создайте новый проект приложения в Xcode и выберите SwiftUI App for Lifecycle в мастере установки. Вот и все!
Для приложения SwiftUI нет единого названия. Как вы скоро увидите, с точки зрения кода это структура, соответствующая новому протоколу приложений. В Xcode его часто называют приложением SwiftUI или жизненным циклом приложения SwiftUI. В этом уроке мы будем называть его под разными именами, но, скорее всего, чаще всего как «Приложение SwiftUI» (заглавная буква «A»).
Одной из обязанностей приложения SwiftUI, а ранее и представителя приложения, является настройка вашего приложения. Вам нужно настроить среду приложения таким образом, чтобы оно работало так, как должно. Например, указав приложению контекст Core Data.
Вот, проверьте это:
@основной
структура CardsApp: Приложение
{
пусть persistenceController = PersistenceController.preview
var body: некоторая сцена {
ОкноГруппа {
Список карт()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
В приведенном выше коде используется шаблон PersistenceController (шаблон из Xcode) для внедрения экземпляра NSManagedObjectContext в представление CardList. Это распространенный подход к предоставлению доступа представления SwiftUI к контейнеру Core Data, но это выходит за рамки этого примера.
В приведенном выше коде вы видите две важные вещи:
- Структура CardsApp имеет свойство persistenceController, которое инициализируется значением по умолчанию. Когда структура инициализируется, это свойство тоже.
- Свойство используется для внедрения значения в среду представления CardList (и иерархию) с модификатором Environment(_:_:).
В этом примере контейнер Core Data (и контроллер постоянства) необходимо инициализировать до того, как представление CardList отобразится на экране. Вот почему свойство добавляется в структуру CardsApp. Когда инициализируется экземпляр этой структуры, инициализируется и контроллер персистентности — как раз перед тем, как пользовательский интерфейс отображается на экране.
Вот еще один пример:
@основной
структура BooksApp: Приложение {
в этом() {
Багснаг.start()
}
var body: некоторая сцена {
}
}
В приведенном выше коде мы инициализируем службу отчетов о сбоях Bugsnag. Это происходит в функции инициализатора init() структуры App. При инициализации приложения вызывается функция Bugsnag.start(), которая дополнительно настроит интеграцию с сервисом Bugsnag.
Как и раньше, точный пример не так важен, но важен подход, используемый для установки зависимостей. Если ваша цель — подготовить приложение к запуску, init() — отличное место для настройки служб, параметров конфигурации и т. д.
Теперь, когда вы настроили свое приложение, вам нужно будет реагировать на изменения жизненного цикла. Мы уже обсуждали их раньше: что происходит, например, когда ваше приложение сворачивается пользователем?
Проверь это:
@основной
структура BooksApp: Приложение {
@Environment(\.scenePhase) частная переменная ScenePhase
var body: некоторая сцена {
ОкноГруппа {
Список книг()
}
.onChange(of: ScenePhase) { фаза
если фаза == .background {
// очищаем ресурсы, останавливаем таймеры и т.д.
}
}
}
}
В приведенном выше коде мы используем несколько основных принципов SwiftUI. Короче говоря, модификатор onChange(of:perform:) вызывает свое закрытие каждый раз, когда меняется «фаза» ScenePhase. Эта фаза сцены представляет собой значение среды, доступное как для экземпляров приложения, так и для представления.
Перечисление ScenePhase имеет 3 состояния:
- active – сцена находится на переднем плане и активна
- неактивен – сцена на переднем плане, но неактивна
- фон — сцена в настоящее время не видна в пользовательском интерфейсе
Переход от одной фазы к другой является событием жизненного цикла. Он рассказывает вам кое-что о том, что происходит в «жизни» приложения. Например, фаза вашего приложения меняется на неактивную, когда вы запускаете переключатель приложений.
Представьте, что мы запускаем приведенный ниже код. В общем, попробуйте сами!
.onChange(of: ScenePhase) { фаза
печать (фаза)
}
Этот код эффективно распечатывает каждое изменение фазы сцены для приложения. Вот что вы найдете:
- Запуск приложения: активно
- Вызов переключателя приложений: активное/неактивное
- Сворачивание или закрытие приложения: фон
- Открытие приложения из App Switcher: из неактивного в активное
Здесь важно отметить, что iOS контролирует, когда и когда происходит изменение фазы. Вы не можете рассчитывать на то, что приложение будет активным в какой-то момент, а затем всегда будет проходить неактивную фазу, прежде чем перейти в фоновый режим и закрыть приложение. Не полагайтесь на точный порядок фаз.
Вместо этого используйте фазы сцены и события жизненного цикла, чтобы реагировать способом, подходящим для вашего приложения. Несколько примеров:
- Остановите игру или интенсивные вычисления, когда фаза неактивна.
- Отменяйте таймеры, когда фаза неактивна, и (пере) запускайте активную фазу.
- Остановить сетевые запросы и закрыть открытые файлы в фоновом режиме
И последнее, но не менее важное: вы можете наблюдать этапы сцены как в представлении, так и в приложении. Внутри структуры App вы получаете обновления для каждой сцены, подключенной к приложению. Внутри представления вы получаете обновления только для той сцены, частью которой является представление. Это на самом деле полезно, потому что вы можете, скажем, отменить таймер «глубоко» внутри приложения, прямо внутри представления, которое использует этот таймер.
Note: Назвать «фоновую» фазу сложно. Думайте об этом как о том, что «это приложение скоро закроется», а не как о том, что «это приложение работает в фоновом режиме». Как мы уже говорили, iOS контролирует эти этапы. Вполне возможно, что ваше приложение работает в фоновом режиме, поскольку оно все еще отображается в переключателе приложений, тогда как на самом деле приложение закрывается, поскольку пользователь какое-то время его не использовал.
Еще не готовы отказаться от любимого App Delegate? Не волнуйтесь, вы все равно можете использовать его в приложении SwiftUI через оболочку свойства @UIApplicationDelegateAdaptor!
Проверь это:
@основной
структура ParseApp: Приложение
{
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: некоторая сцена {
ОкноГруппа {
КонтентПросмотр()
}
}
}
класс AppDelegate: NSObject, UIApplicationDelegate
{
func application (_ application: UIApplication, DidFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = ноль) -> Логическое значение
{
ParseSwift.initialize(applicationId: «», serverURL: URL (строка: «http://localhost:1337/parse»)!)
вернуть истину
}
}
О, НЕТ! Это делегат приложения!?
В приведенном выше коде мы, по сути, указали структуре App инициализировать экземпляр AppDelegate, присвоить его свойству appDelegate и считать этот объект делегатом нашего приложения. Все это происходит через оболочку свойства @UIApplicationDelegateAdaptor.
Вы видите, что класс AppDelegate соответствует NSObject и UIApplicationDelegate, как и обычный делегат приложения. Это также означает, что вы можете использовать любую доступную функцию делегата, например application(_:didFinishLaunchingWithOptions:).
Вы можете использовать UIApplicationDelegateAdaptor для любых функций, которые еще не доступны в протоколе приложения, например для удаленных push-уведомлений. На данный момент это работает точно так же, как и всегда.
Здесь стоит отметить, что хотя вы можете использовать делегат приложения для чего угодно, разумно использовать его только для функций, которых вы не можете достичь с помощью одной только структуры App. В приведенном выше примере для Parse Server мы могли бы сделать следующее:
@основной
структура ParseApp: Приложение
{
в этом() {
ParseSwift.initialize()
}
var body: какая-то сцена
}
Это избавляет нас от большого количества шаблонного кода, что является одним из наиболее важных преимуществ использования жизненного цикла приложения SwiftUI!
Структура и жизненный цикл приложения SwiftUI интересны, не так ли? Это начало нового подхода к загрузке и настройке «чистых» приложений SwiftUI. Несмотря на то, что структура App не поддерживает все функции, которые традиционно имеют делегаты приложений и сцен, это отличный способ настроить простое приложение SwiftUI.
Вот что мы обсудили:
- Как настроить приложение SwiftUI и структуру приложения?
- Работа с @main, сценами и событиями жизненного цикла.
- Настройка среды и служб приложения
- Использование @UIApplicationDelegateAdaptor в качестве резервного делегата приложения