Казалось бы, у вас в кармане суперкомпьютер. Он точно должен уметь выполнять рутинные задачи в фоне, пока вы занимаетесь своими делами. Как cron в Unix, который отлично работает с 1970 года. Но реальность iOS оказалась сложнее.
Автор статьи делится опытом работы с BGTaskScheduler - фреймворком для запуска фоновых задач. Методом проб и ошибок, работая с собственным устройством ему удалось добиться приемлемого уровня надежности фонового выполнения.
Типы фоновых задач в iOS:
Есть три основных типа фоновых задач:
🔵BGAppRefreshTask: короткая задача для обновления. Предназначена для поддержания контента приложения в актуальном состоянии.
🔵BGProcessingTask: для более тяжелой работы, которая может занимать минуты.
🔵BGContinuedProcessingTask: появился в iOS 26. Запускается на переднем плане и может продолжать работу в фоне. Для автоматической синхронизации бесполезен.
Apple не публикует точных числовых ограничений. Реальные лимиты приходится выяснять вручную.
Как запланировать задачи:
Для этого необходимо отправить запрос и зарегистрировать обработчик:
let refresh = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
refresh.earliestBeginDate = lastRun.addingTimeInterval(30 * 60)
try BGTaskScheduler.shared.submit(refresh)
let processing = BGProcessingTaskRequest(identifier: "com.app.process")
processing.earliestBeginDate = lastRun.addingTimeInterval(60 * 60)
try BGTaskScheduler.shared.submit(processing)
В обработчике обязательно нужно задать expirationHandler. Если этого не сделать, iOS отметит задачу как завершенную с проблемой.
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.process") { task in
task.expirationHandler = {
cancelWork()
task.setTaskCompleted(success: false)
}
Task {
let ok = await doWork()
task.setTaskCompleted(success: ok)
}
}Точность планирования:
BGTaskScheduler не похож на cron. Задача может запуститься на несколько часов позже запрошенного времени. На устройстве автора processing задачи чаще запускались ночью на зарядке, а refresh днем, когда телефон активно использовался.
Полный цикл синхронизации укладывался в секунду. Это позволило зарегистрировать оба типа задач, чтобы дать iOS больше возможностей для запуска.
Ошибка: выполнение тяжелых операций в refresh задаче. После этого пробуждения стали менее надежными. Apple объясняет это энергетическим бюджетом приложения. Чем больше энергии и трафика потребляет задача, тем реже iOS ее запускает.
Подводные камни планирования:
BGTaskScheduler хранит только один ожидающий запрос для каждого идентификатора. Отправка нового запроса заменяет предыдущий. Если не проверять существующий запрос, можно бесконечно отодвигать задачу по времени.
static func shouldSkipSubmit(existingEarliest: Date?, desiredEarliest: Date) -> Bool {
guard let existingEarliest else { return true }
return existingEarliest <= desiredEarliest.addingTimeInterval(5)
}Важно вычислять время от фиксированной точки, а не от текущего момента.
let desiredEarliest = max(
now.addingTimeInterval(60),
lastRun.addingTimeInterval(interval)
)
earliestBeginDate - это нижняя граница, а не расписание. iOS не гарантирует запуск в указанное время.
🔗 Читать подробнее
💡 Вывод:
BGTaskScheduler не надежен как cron. Но добиться стабильного расписания можно. Планируйте задачи с фиксированной точки отсчета. Не пытайтесь бороться с iOS - она сама решает, когда запускать задачи. Используйте несколько типов фоновых задач, чтобы увеличить шансы на запуск. И помните: поведение пользователя и состояние устройства влияют на выполнение сильнее, чем ваш код.
Подписаться на канал:
➡️ Telegram | Max
