Nadzeya Savitskaya 19 января 2023

📱Как работают таймлайны и как обновлять виджеты правильно

В этой статье подробно рассмотрены возможности обновления контента в 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 есть еще следующие:

  1. .never – новый таймлайн-провайдер не создастся по истечении текущего, то есть контент на виджете больше не будет меняться.
  2. .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-разработчик.

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Go-разработчик
по итогам собеседования
Разработчик С#
от 200000 RUB до 400000 RUB

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ