Irbisa · Шпаргалка

Жизненный цикл, фон и push

Холодный старт, scene phase, фоновые режимы, BGTaskScheduler, APNs, silent push, universal links

Middle-разработчик · 23 вопроса

  1. 01

    В каких состояниях может быть iOS-приложение (not running, inactive, active, background, suspended), и что переводит его из одного в другое?

    Приложение бывает not running (не запущено), inactive (на экране, но без событий), active (на экране и получает события), background (не на экране, код ещё работает) или suspended (в памяти, ничего не выполняет).

    final class SceneDelegate: UIResponder, UIWindowSceneDelegate {  var window: UIWindow?   // background -> inactive: about to return to the screen  func sceneWillEnterForeground(_ scene: UIScene) { feed.refreshIfStale() }…
  2. 02

    Чем AppDelegate отличается от SceneDelegate, и зачем в iOS 13 жизненный цикл UI перенесли в сцены?

    AppDelegate отвечает за процесс: запуск, push-токены, события фонового URLSession, выбор конфигурации сцены.

    @mainfinal class AppDelegate: UIResponder, UIApplicationDelegate {  // Once per process, with or without UI  func application(_ application: UIApplication,                   didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {    Analytics.configure()…
  3. 03

    Как отслеживать жизненный цикл приложения в SwiftUI через scenePhase и когда всё ещё нужен @UIApplicationDelegateAdaptor?

    Читайте @Environment(\.scenePhase) и реагируйте в .onChange(of:): .active, .inactive и .background соответствуют состояниям сцены в UIKit, а @UIApplicationDelegateAdaptor возвращает AppDelegate для колбэков, для которых в SwiftUI нет модификатора.

    @mainstruct ShopApp: App {  @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate  @Environment(\.scenePhase) private var scenePhase  @State private var store = CartStore()…
  4. 04

    Гарантированно ли вызывается applicationWillTerminate и где тогда сохранять состояние?

    Нет. Он вызывается, только если приложение завершают, пока оно выполняется, а большинство умирает замороженными, без всякого колбэка.

  5. 05

    Чем локальные уведомления отличаются от удалённых и как запросить у пользователя разрешение?

    Локальное уведомление планирует само приложение на устройстве с помощью триггера; удалённое отправляет ваш сервер через APNs на device token.

    import UIKitimport UserNotifications @MainActorfunc enableReminders() async throws {  let center = UNUserNotificationCenter.current()…
  6. 06

    После выхода этого кода число сессий удвоилось. Почему и в каком колбэке должна начинаться сессия?

    final class SceneDelegate: UIResponder, UIWindowSceneDelegate {  var window: UIWindow?   func sceneDidBecomeActive(_ scene: UIScene) {    Analytics.log("session_start")  }   func sceneDidEnterBackground(_ scene: UIScene) {    Analytics.log("session_end")  }}

    sceneDidBecomeActive срабатывает заново после каждого прерывания, которое лишь делает приложение inactive (запроса Face ID, Control Center, системного алерта), поэтому логирует лишние старты без парного конца.

  7. 07

    Что происходит между тапом пользователя по иконке и вызовом application(_:didFinishLaunchingWithOptions:)?

    Система создаёт процесс, dyld загружает динамические библиотеки, отрабатывают рантайм Objective-C и статические инициализаторы, затем main вызывает UIApplicationMain, который создаёт UIApplication и ваш app delegate и только после этого вызывает willFinishLaunching и didFinishLaunching.

  8. 08

    Как iOS ограничивает фоновую работу и какими легальными способами можно выполнить код после того, как пользователь ушёл из приложения?

    После ухода пользователя приложение получает несколько секунд в фоне, а затем замораживается.

    <!-- Info.plist: declare only what the app genuinely does --><key>UIBackgroundModes</key><array>  <!-- lets the system wake you for BGAppRefreshTask -->  <string>fetch</string>  <!-- lets the system wake you for BGProcessingTask -->…
  9. 09

    Этот код должен дать загрузке завершиться после ухода пользователя из приложения. Что с ним не так?

    func sceneDidEnterBackground(_ scene: UIScene) {  let app = UIApplication.shared  let id = app.beginBackgroundTask(withName: "upload")   uploader.upload(draft) { result in    if case .success = result {      app.endBackgroundTask(id)    }  }}

    Нет expiration handler, а задача завершается только при успехе, поэтому медленная или упавшая загрузка оставляет идентификатор открытым; когда бюджет заканчивается, watchdog убивает приложение с кодом 0x8badf00d, и пользователь видит это как краш при переключении на другое приложение.

  10. 10

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

    struct WorkoutTimerView: View {  @State private var elapsed = 0  private let ticker = Timer.publish(every: 1, on: .main, in: .common).autoconnect()   var body: some View {    Text(Duration.seconds(elapsed).formatted(.time(pattern: .minuteSecond)))      .monospacedDigit()      .onReceive(ticker) { _ in        elapsed += 1      }  }}

    Вью считает тики, а не измеряет время.

  11. 11

    Как запланировать работу через BGTaskScheduler и почему зарегистрированная задача может так и не запуститься?

    Зарегистрируйте обработчик до возврата из didFinishLaunching, добавьте идентификатор в Info.plist, затем вызовите submit с BGAppRefreshTaskRequest или BGProcessingTaskRequest.

    // Register before didFinishLaunching returnsfunc application(_ application: UIApplication,                 didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {  BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.app.nightly-sync",                                  using: nil) { task in    handleSync(task as! BGProcessingTask)…
  12. 12

    Как протестировать задачу BGTaskScheduler, не дожидаясь, пока её запустит система?

    На устройстве уведите приложение в фон после того, как оно отправило запрос, поставьте паузу в Xcode и вызовите из LLDB приватный _simulateLaunchForTaskWithIdentifier:, и зарегистрированный обработчик выполнится сразу.

    # 1. Run on a device from Xcode, let the app call submit(),#    then send it to the background.# 2. Press Pause in Xcode and enter in the LLDB console: e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.app.nightly-sync"]…
  13. 13

    Как push-уведомления работают от начала до конца: APNs, device token и payload?

    Приложение запрашивает у APNs device token и отправляет его на ваш сервер; сервер шлёт JSON-payload для этого токена в APNs по HTTP/2, аутентифицируясь ключом .p8 или сертификатом, а APNs доставляет его, чтобы iOS показала уведомление или разбудила приложение.

    func application(_ application: UIApplication,                 didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {  // Every launch: the token may have changed since last time.  application.registerForRemoteNotifications()  return true}…
  14. 14

    Что нужно silent push, чтобы разбудить приложение, и почему он всё равно может не дойти?

    В payload нужен "content-available": 1 и ничего видимого, в запросе нужны apns-push-type: background и apns-priority: 5, а приложению нужен фоновый режим remote-notification.

    // What the server sends://   apns-push-type: background   <- alert here means no background wake//   apns-priority: 5             <- 10 is not allowed for background pushes//   apns-topic: com.example.app//   { "aps": { "content-available": 1 }, "reason": "feed" }…
  15. 15

    Найдите баг в этом обработчике silent push: пуш приходит, но когда пользователь открывает приложение, входящие всё ещё устаревшие.

    func application(_ application: UIApplication,                 didReceiveRemoteNotification userInfo: [AnyHashable: Any],                 fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {  guard userInfo["reason"] as? String == "inbox" else {    completionHandler(.noData)    return  }  Task {    try await inbox.sync()  }  completionHandler(.newData)}

    completionHandler(.newData) вызывается сразу после создания Task, до того как синхронизация что-то сделала.

  16. 16

    Как показать или скрыть уведомление, пришедшее при открытом приложении, и как тапы и кнопки действий попадают в userNotificationCenter(_:didReceive:)?

    Реализуйте userNotificationCenter(_:willPresent:), чтобы решать, показывать ли уведомление на переднем плане, и зарегистрируйте при запуске UNNotificationCategory с действиями; ключ category в payload выбирает кнопки, а userNotificationCenter(_:didReceive:) обрабатывает тапы и действия.

    func application(_ application: UIApplication,                 didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {  let reply = UNTextInputNotificationAction(identifier: "REPLY", title: "Reply", options: [],                                            textInputButtonTitle: "Send", textInputPlaceholder: "Message")  let message = UNNotificationCategory(identifier: "MESSAGE",   // "category": "MESSAGE" in aps                                       actions: [reply], intentIdentifiers: [])…
  17. 17

    Что даёт provisional-разрешение на уведомления и почему пользователь со статусом .authorized может ничего не видеть?

    Если добавить .provisional в requestAuthorization, разрешение выдаётся без диалога, но уведомления тихо попадают в Центр уведомлений, без баннера и звука, пока пользователь не нажмёт «Оставить».

    let center = UNUserNotificationCenter.current() func requestProvisional() async throws {  // Day one: no prompt. Delivered quietly to Notification Center.  let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])  // granted is true immediately; the user saw nothing.…
  18. 18

    Чем universal links отличаются от кастомных URL-схем и что нужно universal link, чтобы открыть приложение?

    Кастомная схема вроде myapp:// ничем не подтверждена (её может заявить любое приложение, и она не срабатывает, если приложение не установлено), а universal link работает как обычный https-URL, который открывает приложение только после того, как iOS проверила домен, а иначе открывает сайт.

    {  "applinks": {    "details": [      {        "appIDs": ["ABCDE12345.com.example.app"],        "components": […
  19. 19

    Universal links открывают нужный экран, пока приложение запущено, но после холодного старта приводят на главный экран. Чего здесь не хватает?

    final class SceneDelegate: UIResponder, UIWindowSceneDelegate {  var window: UIWindow?   func scene(_ scene: UIScene, willConnectTo session: UISceneSession,             options connectionOptions: UIScene.ConnectionOptions) {    guard let windowScene = scene as? UIWindowScene else { return }    window = UIWindow(windowScene: windowScene)    window?.rootViewController = RootViewController()    window?.makeKeyAndVisible()  }   func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {    guard let url = userActivity.webpageURL else { return }    router.open(Route(url) ?? .home)  }}

    При холодном старте ссылка приходит не через scene(_:continue:): она лежит в connectionOptions.userActivities внутри scene(_:willConnectTo:options:), а этот код её игнорирует.

  20. 20

    Как iOS может запустить приложение сразу в фоне и чего didFinishLaunching должен избегать на этом пути?

    Silent push, задача BGTaskScheduler, завершившаяся фоновая URLSession, событие геолокации или Bluetooth могут запустить процесс без UI.

    func application(_ application: UIApplication,                 didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {  // Needed on every launch, visible or not.  registerBackgroundTasks()  UNUserNotificationCenter.current().delegate = self  return true…
  21. 21

    Какие фоновые пробуждения ещё доходят до приложения после того, как пользователь смахнул его в переключателе приложений?

    Принудительное закрытие говорит iOS, что пользователь хочет остановить приложение: silent push, задачи BGTaskScheduler, передачи фоновой URLSession и перезапуски по геолокации прекращаются, пока пользователь снова не откроет приложение, а alert-push и VoIP push по-прежнему доходят.

  22. 22

    Что такое prewarming приложения и как он искажает метрики времени запуска?

    Начиная с iOS 15 система может запустить процесс заранее, перед предсказанным запуском, выполнить dyld и статические инициализаторы и остановиться перед UIApplicationMain.

    import UIKitimport os @MainActorenum LaunchClock {  // Set by the system only when this process was started by prewarming.…
  23. 23

    Что такое BGContinuedProcessingTask в iOS 26 и когда его выбирать вместо BGProcessingTask?

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

    import BackgroundTasks func startExport(_ video: Video) throws {  // Info.plist lists com.example.app.export.* in BGTaskSchedulerPermittedIdentifiers  let id = "com.example.app.export.\(video.id)"…