На WWDC 2024 Apple представила — виджеты в Control Center для iOS 18. Это новшество позволяет разработчикам добавлять собственные виджеты в новое место в приложении: Control Center, Home Screen. Но можем ли мы делать кастомную вёрстку в новых виджетах? Или подтянуть данные из сети?
В этой статье разберёмся с новыми виджетами, ответим на вопросы выше. А в завершении статьи вы найдёте сниппеты кода, чтобы быстро добавить виджеты в свой проект.
Как добавить виджет в проект
Поскольку виджет считается отдельным приложением, необходимо добавить таргет extension в проект:
-
Добавляем таргет
File → New → Target → Во вкладке iOS выбираем Widget Extension
-
Конфигурируем виджет
Называем таргет любым названием и нажимаем галочки в пунктах:
-
Include Control
-
Include Configuration App Intent
-
Готово. Виджет добавлен в проект.
Какие виджеты доступны
Control Widgets доступны в двух вариациях: Button и Toggle. Такое ограничение было представлено Apple с iOS 17, когда были представлены интерактивные виджеты (подробнее можно почитать в статье — Пишем интерактивный виджет)
Виджет с Button позволяет выполнить любой action через AppIntents. Чаще всего это будет редирект в приложение в определённый функционал по url_scheme. Далее в разделе с AppIntents будет приведён пример редиректа.
Виджет с Toggle позволяет переключаться между состояниями: true или false (вкл или выкл). Очевидный пример с фонариком. Apple также приводит пример с таймером, где можно настроить виджет через динамичную конфигурацию, задав время таймеру и через виджет уже запускать таймер.
В обоих случаях взаимодействие осуществляется через AppIntent. Разберёмся с библиотекой подробнее.
AppIntents
Библиотека App Intents позволяет расширить функционал приложения, интегрируя с экосистемными фичами приложения: Siri, Spotlight, Shortcuts app. После настройки intent’a (или намерения) и добавления в экосистему, Apple начнёт рекомендовать пользователям удобные шорткаты для использования.
Для продолжения работы с виджетом, нам хватит понимания, что App Intent — это action, для Button экшн открывает основное приложение, для Toggle — переключает состояния виджета. Выполнение action в intent происходит в асинхронном методе func perform() async
struct HelloWorldIntent: AppIntent { static var title: LocalizedStringResource = "Hello to the World" func perform() async throws -> some IntentResult { print("Hello world") return .result() } }
Через AppIntents в будущем можно будет сделать настройку, чтобы через Siri выполнять нужные действия в приложении, например, переключить Toggle: включить или выключить фонарик.
Redirect в приложение
Во время реализации виджета, может возникнуть вопрос, как перенаправить пользователя из виджета iOS в другое приложение. Ведь способ с UIApplication.shared.open(url)
и UIApplication.shared.open(url)
— не подойдёт, поскольку мы не имеем доступ к shared
инстансу приложения из виджет‑таргета.
На помощь приходит OpensIntent
, интент доступный с iOS 16, цель которого — перенаправить action с url‑схемой в приложение или перейти по диплинку.
struct OpenAppIntent: AppIntent { static var title: LocalizedStringResource = "Открывает приложение" static var isDiscoverable: Bool = false static var openAppWhenRun: Bool = true func perform() async throws -> some IntentResult & OpensIntent { // url-scheme в соответствии с вашим приложением .result(opensIntent: OpenURLIntent(URL(string: "fichaApp://")!)) } }
Этот AppIntent отлично подойдёт для Button виджета.
Для Toggle виджета будет необходим SetValueIntent
, по которому будет определяться локальное состояние true
или false
.
struct ToggleAppIntent: SetValueIntent { @Parameter(title: "Running") var value: Bool static var title: LocalizedStringResource = "Toggle Control Widget" static var isDiscoverable: Bool = false static var openAppWhenRun: Bool = true func perform() async throws -> some IntentResult { // Будет меняться в зависимости от значения. print(value) return .result() } }
Button виджет
Виджет‑кнопка состоит из трёх составляющих:
-
Конфигурация виджета. В нашем случае
StaticControlConfiguration
-
Вью виджета — ControlWidgetButton, в которой можно указать image, title, subtitle
-
AppIntent, по которому будет происходить переход в приложение
struct ShortcutButtonControlWidget: ControlWidget { let kind: String = "widget.ShortcutControlButtonWidget" var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: kind) { ControlWidgetButton(action: OpenAppIntent()) { Label("Ficha", image: "ficha-logo-control") } } } }
Button виджет также как и Toggle виджет может иметь 3 размерные вариации: small, medium, large. Делать какую‑то кастомную вёрстку — нельзя, Apple не предоставила сторонним разработчикам такого функционала.
После добавления конфигурации и AppIntens, который мы написали выше, получаем виджет, по нажатию на который происходит в переход в приложение.
P.S. На момент написания статьи переход в приложение работал нестабильно на Xcode 16 Beta 2 и iOS 18.0. В будущих версиях скорее всего будет исправлено.
Toggle виджет
Напишем Toggle‑виджет, который будет сохранять своё состояние вкл / выкл в UserDefaults, чтобы это состояние можно было шарить между основным приложением.
Виджет‑переключатель состоит из четырёх составляющих:
-
Конфигурация виджета. В нашем случае
StaticControlConfiguration
-
Вью виджета — ControlWidgetToggle, в которой можно указать image, title, subtitle. А также получить состояние isOn
-
AppIntent, по которому будет происходить переключение состояния виджета
-
ControlValueProvider — проводник значения. С возможностью подтягивать состояние виджета из сети и возможности шарить на другие девайсы
struct Provider: ControlValueProvider { typealias Value = Bool var previewValue: Value { false } func currentValue() async throws -> Value { ToggleStateManager.shared.isOn = !ToggleStateManager.shared.isOn let value = ToggleStateManager.shared.isOn return value } } struct ShortcutToggleControlWidget: ControlWidget { let kind: String = "widget.ShortcutControlToggleWidget" var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: kind, provider: Provider()) { isRunning in ControlWidgetToggle("Ficha", isOn: isRunning, action: ToggleAppIntent(), valueLabel: { isOn in Label(isOn ? "true" : "false", image: "ficha-logo-control") }) .tint(.purple) } } }
Изменить цвет иконки можно с помощью модификатора .tint(.purple)
. Аналогичное изменение цвета не работает для Control виджета с Button.
Интересным элементом Toggle виджета является ControlValueProvider
. Данный протокол имеет:
-
var previewValue
— захардкоженное значения для превью в галерее виджетов -
func currentValue() async throws -> Self.Value
— асинхронный метод
С помощью метода появляется возможность хранить состояние виджета в сети. На примере, которые демонстрирует Apple, состояние подтягивается из сети и шарится между всеми девайсами Apple, что создаёт ощущение бесшовности в использовании экосистемы.
ControlCenter
Взаимодействие с Control виджетами из основного приложения осуществляется через ControlCenter (аналог WidgetCenter для обычных виджетов).
Перезагрузка виджетов
Для перезагрузки виджета доступно два метода: reloadControls(ofKind: )
reloadAllControls()
import WidgetKit // Перезагружает контрол виджет с определённым kind. // В данном случае перезагрузит Toggle виджет ControlCenter.shared.reloadControls(ofKind: "ShortcutControlToggleWidget") // Перезагружает все контрол виджеты. ControlCenter.shared.reloadAllControls()
Получение добавленных виджетов
Также, через Control Center можно получить текущие добавленные виджеты у пользователя.
import WidgetKit // Получает текущие добавленные control виджеты у пользователя let controls: [ControlInfo] = try await ControlCenter.shared.currentControls()
ControlInfo
— структура в которой содержатся данные:
-
kind
— идентификатор виджета (настраивается при создании виджета) -
pushInfo
— опциональное свойство об пуш‑информации (содержит пуш‑токен)
Помимо свойств, существует метод, через который можно получить AppIntent
для конкретного виджета.
Данные о добавленных виджетах могут быть полезны для сбора аналитики.
Итоги
Apple уже четвёртый год подряд, начиная с iOS 14, добавляет что‑то новое в виджеты, показывая тем самым, что этот функционал для них важен и про него не будут забывать. Control виджеты доступны в трёх новых местах iOS:
-
Control Center
-
Lock Screen (Bottom Bar)
-
Кнопка Action (начиная с iPhone 15)
Однако, в iOS 18 добавлен урезаный функционал для Control виджетов: без возможности верстать виджет с кастомным View, как нативный виджет фонарика или Music Control Center.
Надеюсь всё больше компаний обратит на функционал виджетов в iOS, для удобства привёл 2 сниппета для быстрого добавления виджетов.
Сниппет кода виджета
Далее приложены 2 сниппета кода: для Button и Toggle, чтобы любой желающий мог скопировав эти сниппеты быстро добавить в проект Control виджеты.
(Как добавить сам таргет‑виджета, написано в этой статье выше)
Button Control Widget
import WidgetKit import SwiftUI import AppIntents @available(iOSApplicationExtension 18.0, *) struct OpenAppIntent: AppIntent { static var title: LocalizedStringResource = "Button Control Widget" static var isDiscoverable: Bool = false static var openAppWhenRun: Bool = true func perform() async throws -> some IntentResult & OpensIntent { let defaultIntent = OpenURLIntent() guard let url = URL(string: "fichaApp://") else { return .result(opensIntent: defaultIntent) } return .result(opensIntent: OpenURLIntent(url)) } } @available(iOSApplicationExtension 18.0, *) struct ShortcutButtonControlWidget: ControlWidget { let kind: String = "widget.ShortcutControlButtonWidget" var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: kind) { ControlWidgetButton(action: OpenAppIntent()) { Label("Ficha", image: "ficha-logo-control") } } } }
Toggle Control Widget
import WidgetKit import SwiftUI import AppIntents public class ToggleStateManager { static let shared = ToggleStateManager() private let key = "widget.ShortcutControlToggleWidget" public var isOn: Bool { get { guard let boolValue = UserDefaults.standard.object(forKey: self.key) as? Bool else { return false } return boolValue } set { UserDefaults.standard.set(newValue, forKey: self.key) } } } @available(iOSApplicationExtension 18.0, *) struct Provider: ControlValueProvider { typealias Value = Bool var previewValue: Value { false } func currentValue() async throws -> Value { ToggleStateManager.shared.isOn = !ToggleStateManager.shared.isOn let value = ToggleStateManager.shared.isOn return value } } struct ToggleAppIntent: SetValueIntent { @Parameter(title: "Running") var value: Bool static var title: LocalizedStringResource = "Toggle Control Widget" static var isDiscoverable: Bool = false static var openAppWhenRun: Bool = true func perform() async throws -> some IntentResult { ToggleStateManager.shared.isOn = value return .result() } } @available(iOSApplicationExtension 18.0, *) struct ShortcutToggleControlWidget: ControlWidget { let kind: String = "widget.ShortcutControlToggleWidget" var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: kind, provider: Provider()) { isRunning in ControlWidgetToggle("Some title", isOn: isRunning, action: ToggleAppIntent(), valueLabel: { isOn in Label(isOn ? "true" : "false", image: "image-name") }) .tint(.purple) } } }
Полезные материалы
ссылка на оригинал статьи https://habr.com/ru/articles/827868/
Добавить комментарий