На 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)
}
}
}