📱Как работают таймлайны и как обновлять виджеты правильно
В этой статье подробно рассмотрены возможности обновления контента в Home Screen и Lock Screen виджетах для iOS 16.
В iOS 14 Apple представила Home Screen Widgets. Можно было бы сказать, что так впервые появились виджеты, но это не правда – они и до этого существовали в iOS как Today Extensions, но совершенно не пользовались популярностью. То ли дело взорвавшие тренды Home Screen Widgets. Практически день в день с выходом в релиз новой 14-ой iOS приложения для кастомизации Home Screen завоевали все топы и не опускаются до сих пор, а это уже 2 года. Что же можно о них рассказать интересного и какие есть возможности в работе с ними? Как по мне, не так сложно их сделать, как разобраться в обновлении контента на них. Я предлагаю рассмотреть, что такое таймлайны у виджетов (актуально как для home screen, так и для lock screen) и как их менять в зависимости от вполне реальных задач.
Для обновления контента на виджете создается таймлайн-провайдер – это буквально временная шкала обновлений виджета. Таймлайн-провайдер содержит в себе массив entry-объектов, которые хранят в себе дату и время смены контента и сам контент, который необходимо отобразить. Выходит, что таймлайн-провайдер – это некоторое правило: как часто и с какой информацией будет меняться виджет. Например, мы каждый час будем менять background-картинку на виджете. Пусть у нас будет для этого 10 разных картинок. Вот так будет выглядеть код для этой задачи.
func getTimeline(for configuration: HomeScreenWidgetConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// массив entry объектов
var entries: [SimpleEntry] = []
let currentDate = Date()
// наш контент, который мы будем менять
let images: [Image] = getWidgetImages()
// мы будем менять картинку каждый час 10 раз
for hourOffset in 0 ..< 10 {
// создается время смены картинки
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
// создается entry объект с датой обновления и обновляемым контентом
let entry = SimpleEntry(date: entryDate, configuration: configuration, image: images[hourOffset])
entries.append(entry)
}
// создается timeline provider с массивом созданных entry oбъектов
var timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
Так как мы указали политику обновления policy: .atEnd, WidgetKit запросит новый таймлайн-провайдер после последней даты в entry-объектах. Таким образом, по истечении нашего созданного 10-ти часового timeline provider’а будет создан такой же новый 10 часовой таймлайн-провайдер и наш виджет будет обновлять 10 картинок бесконечно. Кроме .atEnd есть еще следующие:
.never– новый таймлайн-провайдер не создастся по истечении текущего, то есть контент на виджете больше не будет меняться..after(Date())– новый таймлайн создастся после этой даты.
На каждый виджет система выделяет некоторую память, объем которой зависит от множества факторов, один из которых – как часто пользователь пользуется виджетом, то есть смотрит на него. Ресурсы, выделенные системой на один виджет, применяются к 24-часовому периоду, но не к календарным суткам. WidgetKit настраивает 24-часовое окно в соответствии с ежедневной моделью пользования телефоном юзером, что означает, что выделенные ресурсы на день не обязательно сбрасывается ровно в полночь. Для виджета, который пользователь часто просматривает, дневной бюджет ресурсов обычно включает от 40 до 70 обновлений таймлайн-провайдера. Это примерно соответствует перезагрузке виджета каждые 15-60 минут, но на количество обновлений таймлайн-провайдера могут повлиять и другие факторы, значительно их уменьшив. Это означает, что обновлять таймлайн-провайдеры часто мы не можем, а именно обновления чаще 5 минут не поддерживаются. В задачах, где мы точно знаем, на какой контент мы будем обновлять виджет, это ограничение нам ничего и не сделает. Мы все так же сможем обновлять часы каждую минуту, например, вот как будет выглядеть создание таймлайна:
func getTimeline(for configuration: HomeScreenWidgetConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// массив entry объектов
var entries: [SimpleEntry] = []
// для того чтоб минуты обновлялись ровно по часам, дату нужно взять без секунд
let currentDate = Date().zeroSeconds
// мы будем менять значение часов каждую минуту
for minuteOffset in 0 ..< 60 {
// создается время смены картинки
let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: currentDate)!
// создается entry объект с датой обновления
let entry = SimpleEntry(date: entryDate, configuration: configuration)
entries.append(entry)
}
// создается timeline provider с массивом созданных entry oбъектов
var timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
Тут таймлайн мы обновляем каждый час, и внутри таймлайна добавляем 60 entry-объектов на каждую минуту, так мы сможем обновить текст с нужным значением времени. Мы можем добавить и больше 60 entry-объекта – 120, 240 и далее, но и слишком много не получится. Например, при попытке создать entry-объекты сразу на год 60*24*365, бюджет выделенной памяти заполнится и виджет вообще не будет менять информацию. А можем ли мы менять некоторую информацию чаще, чем 1 раз в минуту? Да, можем. Есть два способа это сделать. Если мы хотим виджет с часами с отображением секунд, то во вью самого виджета нам нужно использовать следующее:
var body: some View {
VStack {
Text(entry.date, style: .timer)
}
}
И тогда мы увидим на виджете время такого формата 14:30:25, которое будет меняться каждую секунду и нам не нужно ничего особенного прописывать при создании таймлайна.
Еще мы просто можем задать таймлайн с обновлением каждую секунду, вот так:
func getTimeline(for configuration: HomeScreenWidgetConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// массив entry объектов
var entries: [SimpleEntry] = []
let currentDate = Date()
for secondOffset in 0 ..< 60 {
let entryDate = Calendar.current.date(byAdding: .second, value: secondOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration)
entries.append(entry)
}
// создается timeline provider с массивом созданных entry oбъектов
var timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
Но и тут, конечно же, мы упремся в переполнение выделенной на виджет памяти и в какой-то момент виджет просто перестанет работать, пока память не обновится. Да, это не полностью рабочий вариант, но и он может использоваться, если, например, мы хотим сделать анимационные виджеты, например, гифки. Предсказать точное время работы такого виджета будет крайне сложно, но первые 15-20 секунд точно обеспечены.
Как нам обновлять погодный виджет? Или батарейный? Или любой другой, где мы не можем знать данные наперед и создать сразу таймлайн-провайдер на день/месяц. Мы создаем таймлайн-провайдер с одним entry-объектом и указываем правило обновления – после определенной даты. Пусть это будут 20 минут.
func getTimeline(for configuration: HomeScreenWidgetConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
let currentWeather = getWeather()
let entry = SimpleEntry(date: currentDate, configuration: configuration, weather: currentWeather)
let endDate = Calendar.current.date(byAdding: .minute, value: 20, to: currentDate)!
var timeline = Timeline(entries: [entry], policy: .after(endDate))
completion(timeline)
}
Обновлять таймлайн-провайдер чаще 5 минут мы не можем, соответственно, и обновлять некоторую быстроменяющуюся информацию тоже.
Я постаралась подробно рассказать про работу таймлайн-провайдера и способы обновления виджетов. Надеюсь, что эта статья была полезной. Я буду рада фидбэку и комментариям. Спасибо!
Савицкая Надежда, iOS-разработчик.