Apple планомерно расширяет функционал виджетов с каждым обновлением. Мы уже получили виджеты на Watch и Mac и их разные форматы — на заблокированном экране, live activities. В этом году (WWDC 2023) нам также стали доступны StandBy mode, анимации и возможность управлять виджетом через кнопки.

Меня зовут Даша, я iOS-разработчик в Surf. Сегодня я освежу в вашей памяти философию и design виджетов. Подсвечу технические аспекты, которые помогут, если вы собираетесь делать свой первый виджет или хотите вдохнуть жизнь в старый.
А ещё затрону несколько неочевидных тем:
Взгляд Apple на работу с виджетами.
Ценность виджета для пользователя.
Внутреннее устройство виджета: данные, методы, свойства.
Эффективное обновление виджета.
Философия виджетов
Для начала посмотрим, как Apple представляют идеальную работу с виджетами. Вот вы берете iPhone в руки, чтобы позалипать в cоцсети и замечаете виджет погоды. Вам достаточно одного взгляда (1), чтобы узнать погоду именно в вашем (2) городе — дождь закончится через 15 минут (3) и выглянет солнце.
Мы видим 3 важных принципа, которые должны быть реализованы в хороших виджетах:
At a glance. Не перегружайте виджет функционалом. Одного взгляда должно быть достаточно для восприятия всей необходимой информации. Если пользователь захочет узнать подробный прогноз погоды на неделю или количество солнечных дней, то просто откроет приложение — это ведь можно сделать одним тапом.
Персонализация. Данные должны быть интересны конкретному пользователю. Ему нужна погода, которая за его окном, а не чьим-то еще.
Актуальность. Виджет — не большая статичная иконка приложения. Показывайте данные, интересные пользователю в текущий момент времени.
Кстати, при разработке вы заметите, что вся техническая реализация базируется на привязке ко времени.

Как работают виджеты
Давайте быстро пробежимся по технической реализации виджетов:
Вы предоставляете данные в систему.
Система последовательно отрисовывает UI по этим данным и показывает его на главном экране.
Когда данные заканчиваются, система приходит за новыми — возвращаемся к первому пункту.

Теперь обо всем подробнее.
Timeline
Когда мы говорим о данных, то имеем в виду Timeline
. Timeline
— массив объектов с привязкой ко времени. Каждый объект — это все данные, необходимые для отрисовки виджета, а также время, когда они станут актуальными.
Продолжая пример с погодой, объектом будут температура воздуха, осадки и время (!), на которое эта погода будет актуальна.
Если виджет — View
, то Timeline
— массив ViewModel
, удовлетворяющих протоколу TimelineEntry
. Единственное, что жестко требует TimelineEntry
— это привязка ко времени.
struct WeatherEntry: TimelineEntry {
// TimelineEntry
var date: Date
// Properties
var temperature: Double
var isRaining: Bool
}
TimelineProvider
Мы передаем системе Timeline
, реализуя TimelineProvider
— очередной протокол с методами для генерации timeline’ов. Система же занимается отрисовкой.
Говоря простыми словами, мы пишем «сценарий», когда и что показывать — timeline. А система, используя UI виджета, рисует «комикс» — набор карточек, которые потом видны пользователю. Система сама следит за временем и обновляет виджет.
Учтите, что точное попадание во время, которое вы указали в TimelineEntry date, не гарантировано.
Помимо предоставления timeline, TimelineProvider
просит реализовать ещё два метода:
getSnapshot(in:completion:)
— возвращаетtimelineEntry
для дальнейшей отрисовки скелетона на случай, если настоящие данные еще не подгрузились.placeholder(in:)
— возвращаетtimelineEntry
для галереи виджетов. Этими данными постарайтесь максимально полно продемонстрировать пользователю возможности вашего виджета.
TimelineReloadPolicy
Помимо данных для отображения, у Timeline
есть одно важное свойство — policy
: TimelineReloadPolicy
, определяющее, как система будет получать новые данные. Оно представлено в трёх вариантах:
atEnd
— WidgetKit запросит новый timeline, когда в текущем закончатся данные;after(Date)
— WidgetKit запросит новый timeline, когда наступит определенный момент времени;never
— WidgetKit оставит на экране последние доступные данные и не будет запрашивать новые.
Кроме того, Timeline
может обновиться принудительно, не дожидаясь своего TimelineReloadPolicy
. Нам больше всего интересны два кейса:
Вызов метода
WidgetCenter.shared.reloadTimelines(ofKind: “com.weatherwidget.rainstatus")
в самом приложении.Взаимодействие пользователя с интерактивным виджетом (с iOS 17).
Как эффективно обновлять виджеты
Вы уже могли предположить, что обновление виджета (запрос и расчет новой Timeline
) — трудоемкий процесс. Система накладывает ограничения на частоту обновлений, так что делать запрос на апдейт каждую минуту не получится.
На каждый виджет выделяется «бюджет» — Apple подробно рассказали о нём в документации WidketKit. Если коротко, то количество доступных обновлений в день зависит от «полезности» виджета. Система, например, учитывает:
как часто пользователь заглядывает на экран с вашим виджетом;
когда виджет обновлялся последний раз;
насколько серьезно изменилась геолокация (если используете её).
В документации говорится, что в среднем виджет может рассчитывать всего на 40-70 обновлений в день. Стоит хорошо продумать алгоритм обновления данных и выбор подходящей TimelineReloadPolicy
, чтобы уложиться в лимит.
Хорошая новость: обновления виджета из приложения или из-за взаимодействия пользователя не тратят «бюджета». Старайтесь делать большую часть работы — подготовку и обновление данных — тогда, когда приложение находится в foreground.
Подробный пример оптимизации числа обновлений лежит в документации.
Как попасть в Smart Stack
Помимо частоты обновления ещё интересно, как часто наш виджет будут показывать в Smart Stack.
Пользователь может объединять виджеты в стопки — Smart Stack. Система сама подбирает наиболее актуальный виджет и отображает его первым. Начиная с iOS 15, в Smart Stack могут рекомендоваться виджеты, которые сам пользователь в стопку не добавлял (Widget Suggestions).

Есть несколько способов помочь системе разобраться в том, насколько виджет полезен пользователю в моменте:
1. Задать значение relevance
у каждой TimelineEntry
. Она нужна, чтобы показать, насколько относительно других entries
потенциально важна текущая.
Пример: в течение часа начнется дождь. Пользователь хотел бы узнать об этом до того, как выйдет из дома без зонта. Чтобы ему помочь, для timelineEntry
с информацией об осадках поставим relevance.score
выше остальных. При прочих равных SmartStack переместит виджет погоды наверх стопки, а пользователь останется сухим.
2. Воспользоваться App Intents. Фреймворк позволяет контенту и функциям приложения интегрироваться с системными службами, такими как Siri и Shortcuts. В контексте виджетов нам он особенно полезен, ведь с App Intents система может сама добавить ваш виджет в стопку. Вот как это работает:
Вы «скармливаете» системе информацию о действиях пользователя (intents) внутри приложения.
Система, исходя из собранных предпочтений и привычек пользователя, конфигурирует и показывает ваш виджет.
Пример: у вас есть приложение BookAndFilmAdviser — оно подсказывает, что почитать или посмотреть. Соответственно, у него две конфигурации виджета — с книгами и фильмами. Допустим, пользователь каждую пятницу ищет новый фильм на вечер. Получив информацию об этих действиях через App Intents, iOS создает виджет вашего приложения в конфигурации «фильмы» и отправит его наверх стопки в следующую пятницу.
Как хорошо задизайнить виджет
О дизайне виджетов есть отличное видео с WWDC. Вот главное, что я из него выделила:
Следуйте Human Interface Guidelines
Используйте стандартные шрифты. Во-первых, виджет будет сочетаться с другими иконками на Home screen. Во-вторых, обеспечивает автоматическую поддержку Dynamic type.
Используйте
ContainerRelativeShape
для скругления углов.Тестируйте свой виджет в разных окружениях.
Не добавляйте заголовок — название приложения по умолчанию присутствует под виджетом.
Добавляйте иконку только в том случае, если ваше приложение отображает сторонний контент. Как Apple TV или Podcasts.

В общем, черпайте вдохновение из системных виджетов в iOS. А вот пример хороших не системных виджетов:

Полезные ссылки для вашего виджета — вместо заключения
В конце я бы просто хотела поделиться подборкой полезностей, которые помогут вам создать идеальный виджет. Инструментов и возможностей очень много!
Вы можете создавать виджеты в разных конфигурациях, как в примере про BooksAndFilmsAdvisor.
Даже если вы поддерживаете iOS ниже 17, а обрабатывать нажатия пользователя как-то хочется, можно посмотреть на widgetURL. Ну а если вы всё-таки обновили телефончики, то можете порадовать себя свежими анимациями и настоящими кнопками.
Не забывайте и про адаптацию дизайна под все платформы и окружения — там все не так сложно, как может показаться.
WidgetKit — это не только виджеты, но и Live Activities, Watch Complications.
Надеюсь, всё то, что я собрала для вас, вдохновит на новые идеи и достижения. Скорее садитесь творить!
Больше полезного про iOS — в телеграм-канале Surf iOS Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf. Присоединяйтесь >>
Бонус тем, кто дочитал до конца
Обновлять виджет каждую секунду не получится, но мы можем отобразить тикающий таймер:
var body: some View {
VStack(alignment: .leading) {
Text("Timer").font(.title)
let components = DateComponents(minute: 5)
let futureDate = Calendar.current.date(byAdding: components, to: .now)!
Text(futureDate, style: .timer)
}
}
