WidgetKit iOS 17-26: Інтерактивні Віджети з App Intents
Повний посібник зі створення інтерактивних iOS-віджетів з WidgetKit та App Intents. Охоплює кнопки, перемикачі, анімації, матеріал Liquid Glass iOS 26 та оновлення WWDC 2026.

iOS 17 запровадив нативну інтерактивність до WidgetKit, дозволяючи віджетам реагувати на дії користувача безпосередньо з головного екрана без відкриття застосунку. iOS 26 вдосконалив цю функціональність, додавши матеріал Liquid Glass та акцентовані режими рендерингу. Цей посібник охоплює розробку інтерактивних віджетів від iOS 17 до iOS 26.
Створення інтерактивних віджетів iOS 17-26, від налаштування проєкту до просунутих патернів з анімаціями, керуванням станом та найновішими можливостями App Intents з WWDC 2026.
Архітектура Інтерактивних Віджетів в iOS 17+
Інтерактивність віджетів iOS 17+ працює через фреймворк App Intents. На відміну від традиційних deep-посилань, які відкривали б застосунок, App Intents дозволяють виконувати код безпосередньо з віджета, після чого автоматично оновлюють відображення з новими даними.
import WidgetKit
import SwiftUI
import AppIntents
// The architecture relies on three main components:
// 1. Widget Timeline Provider - supplies the data
// 2. Widget View - displays the interface with Button/Toggle
// 3. App Intent - executes the action on tap
struct TaskWidget: Widget {
// Unique widget identifier
let kind: String = "TaskWidget"
var body: some WidgetConfiguration {
// StaticConfiguration for widgets without parameters
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
TaskWidgetView(entry: entry)
// Required for App Intents since iOS 17
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Tasks")
.description("Manage your tasks from the home screen.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}Віджет оголошує свою конфігурацію та вказує постачальника, який надаватиме дані. Атрибут .containerBackground обов'язковий з iOS 17 для інтерактивних віджетів, а iOS 26 автоматично застосовує стиль Liquid Glass коли це доречно.
Створення Timeline Provider
Timeline Provider визначає, коли і як віджет оновлюється. Для інтерактивних віджетів він також повинен реагувати на зміни, спричинені App Intents.
import WidgetKit
import SwiftUI
// Entry representing widget state at a given moment
struct TaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// Loading state for visual feedback
var isLoading: Bool = false
}
// Data model shared between app and widget
struct Task: Identifiable, Codable {
let id: UUID
var title: String
var isCompleted: Bool
var priority: Priority
enum Priority: String, Codable {
case low, medium, high
}
}
struct TaskTimelineProvider: TimelineProvider {
// Placeholder displayed during initial loading
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(
date: Date(),
tasks: [
Task(id: UUID(), title: "Sample task", isCompleted: false, priority: .medium)
]
)
}
// Snapshot for widget gallery
func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
let entry = TaskEntry(
date: Date(),
tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { $0 }
)
completion(entry)
}
// Complete timeline with refresh policy
func getTimeline(in context: Context, completion: @escaping (Timeline<TaskEntry>) -> Void) {
let tasks = TaskDataManager.shared.fetchTasks()
let entry = TaskEntry(date: Date(), tasks: Array(tasks.prefix(3)))
// Refresh in 15 minutes or after user action
let nextUpdate = Calendar.current.date(
byAdding: .minute,
value: 15,
to: Date()
) ?? Date()
let timeline = Timeline(
entries: [entry],
policy: .after(nextUpdate)
)
completion(timeline)
}
}Постачальник використовує спільний TaskDataManager для доступу до даних. Цей підхід гарантує синхронізацію між головним застосунком і віджетом. Детальніше про пов'язані патерни керування станом читайте у статті SwiftUI @Observable vs @State.
Для обміну даними між застосунком і віджетом потрібно налаштувати App Group у capabilities проєкту. UserDefaults або файли повинні використовувати цю спільну групу.
Менеджер Спільних Даних
Обмін даними між застосунком і віджетом вимагає спільного контейнера, доступного через App Group.
import Foundation
final class TaskDataManager {
// Singleton for global access
static let shared = TaskDataManager()
// App Group identifier configured in Xcode
private let appGroupID = "group.com.example.taskapp"
// UserDefaults shared between app and widget
private var sharedDefaults: UserDefaults? {
UserDefaults(suiteName: appGroupID)
}
private let tasksKey = "tasks"
private init() {}
// Fetch tasks from shared storage
func fetchTasks() -> [Task] {
guard let data = sharedDefaults?.data(forKey: tasksKey),
let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
return []
}
return tasks
}
// Save with widget notification
func saveTasks(_ tasks: [Task]) {
guard let data = try? JSONEncoder().encode(tasks) else { return }
sharedDefaults?.set(data, forKey: tasksKey)
}
// Update a specific task
func updateTask(_ task: Task) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
tasks[index] = task
saveTasks(tasks)
}
}
// Toggle completed state
func toggleTaskCompletion(taskID: UUID) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { $0.id == taskID }) {
tasks[index].isCompleted.toggle()
saveTasks(tasks)
}
}
}Цей менеджер інкапсулює всю логіку персистентності та використовуватиметься як застосунком, так і App Intents віджета.
Створення App Intent для Інтерактивності
App Intent визначає дію, яка виконується, коли користувач взаємодіє з віджетом. iOS виконує цю дію у фоновому режимі, а потім автоматично оновлює віджет.
import AppIntents
import WidgetKit
// Intent to toggle task state
struct ToggleTaskIntent: AppIntent {
// Title displayed in Siri shortcuts
static var title: LocalizedStringResource = "Toggle task state"
// Description for accessibility
static var description = IntentDescription("Marks a task as completed or not completed.")
// Parameter: ID of task to modify
@Parameter(title: "Task ID")
var taskID: String
// Required initializer for AppIntent
init() {}
// Initializer with parameter for view creation
init(taskID: UUID) {
self.taskID = taskID.uuidString
}
// Action execution
func perform() async throws -> some IntentResult {
// Convert string ID to UUID
guard let uuid = UUID(uuidString: taskID) else {
return .result()
}
// Update the task
TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
// Request widget refresh
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}Виклик WidgetCenter.shared.reloadTimelines спричиняє негайне оновлення віджета після дії, гарантуючи миттєвий візуальний зворотний зв'язок.
Готовий до співбесід з iOS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
View Віджета з Інтерактивними Кнопками
View віджета використовує стандартний компонент SwiftUI Button з intent як дію. iOS 17+ автоматично перехоплює ці взаємодії для виконання App Intent.
import SwiftUI
import WidgetKit
struct TaskWidgetView: View {
let entry: TaskEntry
// Adapt to widget size
@Environment(\.widgetFamily) var family
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Header with title and counter
headerView
// Task list with interactive buttons
ForEach(entry.tasks.prefix(tasksLimit)) { task in
TaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
// Number of tasks based on size
private var tasksLimit: Int {
switch family {
case .systemSmall: return 2
case .systemMedium: return 3
default: return 4
}
}
private var headerView: some View {
HStack {
Text("Tasks")
.font(.headline)
.fontWeight(.bold)
Spacer()
// Badge with remaining task count
let remaining = entry.tasks.filter { !$0.isCompleted }.count
Text("\(remaining)")
.font(.caption.bold())
.foregroundStyle(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(remaining > 0 ? Color.orange : Color.green)
.clipShape(Capsule())
}
}
}
struct TaskRowView: View {
let task: Task
var body: some View {
// Button with App Intent as action
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Completion indicator
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// Task title
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
.lineLimit(1)
Spacer()
// Priority indicator
priorityIndicator
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(Color(.systemBackground).opacity(0.5))
.cornerRadius(8)
}
.buttonStyle(.plain)
}
@ViewBuilder
private var priorityIndicator: some View {
switch task.priority {
case .high:
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.red)
case .medium:
Image(systemName: "minus.circle.fill")
.foregroundStyle(.orange)
case .low:
EmptyView()
}
}
}Синтаксис Button(intent:) напряму з'єднує кнопку з App Intent. При дотику iOS виконує perform(), а потім автоматично оновлює віджет.
Інтерактивний Toggle для Віджетів
Для дій типу увімк/вимк компонент Toggle пропонує альтернативу кнопці з нативним стилем iOS.
import SwiftUI
import AppIntents
// Specific intent for Toggle with explicit state
struct SetTaskCompletionIntent: AppIntent {
static var title: LocalizedStringResource = "Set task state"
@Parameter(title: "Task ID")
var taskID: String
// Target state: true = completed, false = not completed
@Parameter(title: "Completed")
var isCompleted: Bool
init() {}
init(taskID: UUID, isCompleted: Bool) {
self.taskID = taskID.uuidString
self.isCompleted = isCompleted
}
func perform() async throws -> some IntentResult {
guard let uuid = UUID(uuidString: taskID) else {
return .result()
}
var tasks = TaskDataManager.shared.fetchTasks()
if let index = tasks.firstIndex(where: { $0.id == uuid }) {
// Set state explicitly (not toggle)
tasks[index].isCompleted = isCompleted
TaskDataManager.shared.saveTasks(tasks)
}
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}
struct TaskToggleRowView: View {
let task: Task
var body: some View {
HStack {
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
Spacer()
// Interactive toggle with intent
Toggle(
isOn: task.isCompleted,
intent: SetTaskCompletionIntent(
taskID: task.id,
isCompleted: !task.isCompleted
)
)
.toggleStyle(.switch)
.labelsHidden()
}
.padding(.vertical, 4)
}
}Toggle забезпечує більш інтуїтивну взаємодію для бінарних станів і природно інтегрується в дизайни iOS.
Віджети не можуть відображати alerts, sheet або навігацію. Усі дії повинні бути автономними та оновлювати видимий стан безпосередньо.
Анімації Оновлення та Переходи
iOS 17+ дозволяє анімувати переходи під час оновлення віджета після дії. Модифікатор .contentTransition керує цими анімаціями.
import SwiftUI
import WidgetKit
struct AnimatedTaskRowView: View {
let task: Task
var body: some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Icon with transition animation
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// Icon animation on change
.contentTransition(.symbolEffect(.replace))
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
// Text animation
.contentTransition(.opacity)
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear)
)
// Background animation
.animation(.easeInOut(duration: 0.3), value: task.isCompleted)
}
.buttonStyle(.plain)
}
}
// Widget with animated invalidation
struct AnimatedTaskWidget: Widget {
let kind: String = "AnimatedTaskWidget"
var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
AnimatedTaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Animated Tasks")
.description("Widgets with smooth animations.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
// Enable content animations
.contentMarginsDisabled()
}
}
struct AnimatedTaskWidgetView: View {
let entry: TaskEntry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
headerView
ForEach(entry.tasks) { task in
AnimatedTaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
private var headerView: some View {
HStack {
Text("Tasks")
.font(.headline.bold())
Spacer()
let completed = entry.tasks.filter(\.isCompleted).count
let total = entry.tasks.count
// Animated progress
Text("\(completed)/\(total)")
.font(.caption.bold())
.foregroundStyle(.secondary)
.contentTransition(.numericText())
}
}
}Анімації .symbolEffect(.replace) та .numericText() створюють плавні переходи між станами, значно покращуючи користувацький досвід.
iOS 26 Liquid Glass та Акцентований Рендеринг
iOS 26 запровадив матеріал Liquid Glass та акцентовані режими рендерингу для віджетів. Ці візуальні оновлення застосовуються автоматично при використанні .containerBackground, але розробники можуть контролювати поведінку рендерингу.
import SwiftUI
import WidgetKit
struct ModernTaskWidgetView: View {
let entry: TaskEntry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
ForEach(entry.tasks.prefix(3)) { task in
TaskRowView(task: task)
}
}
.padding()
// iOS 26: Control accented rendering for images
.widgetAccentedRenderingMode(.fullColor)
}
}
// Extra large portrait family (iOS 26+)
struct LargePortraitTaskWidget: Widget {
let kind: String = "LargePortraitTaskWidget"
var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
LargePortraitTaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Task Overview")
.description("Full task overview with extra space.")
// New in iOS 26: system extra large portrait
.supportedFamilies([.systemExtraLarge])
}
}
struct LargePortraitTaskWidgetView: View {
let entry: TaskEntry
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("All Tasks")
.font(.title2.bold())
// More space for task details
ForEach(entry.tasks) { task in
DetailedTaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
}
struct DetailedTaskRowView: View {
let task: Task
var body: some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(task.isCompleted ? .green : .secondary)
Text(task.title)
.font(.headline)
.strikethrough(task.isCompleted)
}
Text("Priority: \(task.priority.rawValue.capitalized)")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.systemBackground).opacity(0.3))
.cornerRadius(12)
}
.buttonStyle(.plain)
}
}Модифікатор .widgetAccentedRenderingMode(.fullColor) зберігає кольори зображень в акцентованих середовищах. Сімейство .systemExtraLarge, що з'явилося у visionOS 26, тепер доступне на iOS та iPadOS.
WWDC 2026 App Intents: EntityCollection для Продуктивності
WWDC 2026 представило EntityCollection для ефективної обробки великої кількості сутностей. Замість резолвінгу всіх сутностей перед виконанням інтенту передаються лише ідентифікатори.
import AppIntents
import WidgetKit
// Mark multiple tasks complete at once - efficiently
struct BatchCompleteTasksIntent: AppIntent {
static var title: LocalizedStringResource = "Complete multiple tasks"
// EntityCollection passes IDs only, not resolved entities
@Parameter(title: "Tasks")
var tasks: EntityCollection<TaskEntity>
init() {}
init(taskIDs: [UUID]) {
// Create collection from IDs
self.tasks = EntityCollection(taskIDs.map { TaskEntity(id: $0) })
}
func perform() async throws -> some IntentResult {
// Access IDs directly without resolution overhead
let taskIDs = tasks.identifiers
var allTasks = TaskDataManager.shared.fetchTasks()
for id in taskIDs {
if let uuid = UUID(uuidString: id),
let index = allTasks.firstIndex(where: { $0.id == uuid }) {
allTasks[index].isCompleted = true
}
}
TaskDataManager.shared.saveTasks(allTasks)
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}
// Entity conformance for TaskEntity
struct TaskEntity: AppEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Task"
static var defaultQuery = TaskEntityQuery()
var id: UUID
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "Task \(id.uuidString.prefix(8))")
}
}
struct TaskEntityQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
identifiers.map { TaskEntity(id: $0) }
}
}Цей патерн критично важливий для масових операцій. Внутрішні тести Apple показали, що тегування 1000 фотографій змінилося з повільного на "майже миттєве" завдяки EntityCollection.
Довготривалі Інтенти з Відображенням Прогресу
WWDC 2026 також представило LongRunningIntent для операцій, що перевищують стандартний 30-секундний ліміт, з прогресом, що відображається як Live Activity.
import AppIntents
import WidgetKit
struct SyncAllTasksIntent: LongRunningIntent, CancellableIntent {
static var title: LocalizedStringResource = "Sync all tasks"
func perform() async throws -> some IntentResult & ProvidesDialog {
let result = try await performBackgroundTask {
let tasks = TaskDataManager.shared.fetchTasks()
progress.totalUnitCount = Int64(tasks.count)
for (index, task) in tasks.enumerated() {
// Check for cancellation
try Task.checkCancellation()
// Simulate server sync
try await Task.sleep(for: .milliseconds(100))
progress.completedUnitCount = Int64(index + 1)
}
return "Synced \(tasks.count) tasks"
} onCancel: { reason in
// Cleanup based on cancellation reason
switch reason {
case .user:
print("User cancelled sync")
case .timeout:
print("System timeout")
case .systemReclaim:
print("System reclaimed resources")
@unknown default:
break
}
}
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result(dialog: "\(result)")
}
}Прогрес відображається як Live Activity, а CancellableIntent надає причину скасування для належного очищення ресурсів. Детальніше про патерни конкурентності Swift читайте у статті Swift Structured Concurrency.
Налаштовуваний Віджет з AppIntentConfiguration
Для віджетів, що налаштовуються користувачем (фільтри, категорії), AppIntentConfiguration замінює StaticConfiguration.
import WidgetKit
import SwiftUI
import AppIntents
// Configuration exposed to user
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Task configuration"
static var description = IntentDescription("Customize task display.")
// Filter by priority
@Parameter(title: "Priority", default: .all)
var priorityFilter: PriorityFilter
// Show completed tasks
@Parameter(title: "Show completed", default: true)
var showCompleted: Bool
// Maximum number of tasks
@Parameter(title: "Task count", default: 3)
var maxTasks: Int
}
// Enum for priority filter
enum PriorityFilter: String, AppEnum {
case all
case high
case medium
case low
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Priority"
static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
.all: "All",
.high: "High",
.medium: "Medium",
.low: "Low"
]
}
// Provider adapted to configuration
struct ConfigurableTaskProvider: AppIntentTimelineProvider {
typealias Entry = TaskEntry
typealias Intent = TaskWidgetConfigurationIntent
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(date: Date(), tasks: [])
}
func snapshot(for configuration: TaskWidgetConfigurationIntent, in context: Context) async -> TaskEntry {
let tasks = filteredTasks(for: configuration)
return TaskEntry(date: Date(), tasks: tasks)
}
func timeline(for configuration: TaskWidgetConfigurationIntent, in context: Context) async -> Timeline<TaskEntry> {
let tasks = filteredTasks(for: configuration)
let entry = TaskEntry(date: Date(), tasks: tasks)
let nextUpdate = Date().addingTimeInterval(15 * 60)
return Timeline(entries: [entry], policy: .after(nextUpdate))
}
// Apply configuration filters
private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
var tasks = TaskDataManager.shared.fetchTasks()
// Filter by priority
if config.priorityFilter != .all {
let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
tasks = tasks.filter { $0.priority == priority }
}
// Filter completed if needed
if !config.showCompleted {
tasks = tasks.filter { !$0.isCompleted }
}
// Limit count
return Array(tasks.prefix(config.maxTasks))
}
}
// Widget with user configuration
struct ConfigurableTaskWidget: Widget {
let kind: String = "ConfigurableTaskWidget"
var body: some WidgetConfiguration {
// AppIntentConfiguration for configurable widgets
AppIntentConfiguration(
kind: kind,
intent: TaskWidgetConfigurationIntent.self,
provider: ConfigurableTaskProvider()
) { entry in
TaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Custom Tasks")
.description("Filter and customize your tasks.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}Користувач тепер може налаштувати віджет довгим натисканням, отримуючи персоналізований досвід без додаткового коду в застосунку. Детальніше про App Intents та інтеграцію з Siri читайте у статті App Intents 2.0 та Siri Shortcuts.
Найкращі Практики та Оптимізації
Кілька патернів забезпечують продуктивні та надійні інтерактивні віджети.
import WidgetKit
import SwiftUI
// 1. Always invalidate cache after modification
final class WidgetRefreshManager {
static func refreshAllWidgets() {
// Refresh all app widgets
WidgetCenter.shared.reloadAllTimelines()
}
static func refreshWidget(kind: String) {
// Refresh specific widget
WidgetCenter.shared.reloadTimelines(ofKind: kind)
}
// Call from app after data modification
static func notifyDataChanged() {
Task { @MainActor in
refreshAllWidgets()
}
}
}
// 2. Limit view complexity
struct OptimizedWidgetView: View {
let entry: TaskEntry
var body: some View {
// Prefer simple views without GeometryReader
VStack(alignment: .leading, spacing: 8) {
ForEach(entry.tasks.prefix(3)) { task in
// Lightweight components
minimalTaskRow(task)
}
}
.padding()
}
// Minimal view for performance
@ViewBuilder
private func minimalTaskRow(_ task: Task) -> some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
Text(task.title)
.lineLimit(1)
}
}
.buttonStyle(.plain)
}
}
// 3. Use @AppStorage for simple state
struct QuickSettingsWidgetView: View {
// Direct access to shared UserDefaults
@AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
private var showCompleted = true
var body: some View {
// State persists between refreshes
Text(showCompleted ? "Showing all" : "Hiding completed")
}
}
// 4. Pre-calculate data in provider
struct OptimizedTaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// Pre-calculated data
let completedCount: Int
let pendingCount: Int
let highPriorityCount: Int
init(date: Date, tasks: [Task]) {
self.date = date
self.tasks = tasks
// Calculations performed once
self.completedCount = tasks.filter(\.isCompleted).count
self.pendingCount = tasks.filter { !$0.isCompleted }.count
self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.isCompleted }.count
}
}Ці оптимізації забезпечують реактивні віджети, які надмірно не споживають батарею.
Використовуйте схему widget у Xcode для налагодження. Canvas preview дозволяє тестувати різні розміри та стани без встановлення на пристрій.
Джерела
- Документація WidgetKit - офіційний довідник WidgetKit від Apple
- Фреймворк App Intents - повна документація API App Intents
- WWDC 2026: Discover new capabilities in the App Intents framework - EntityCollection, LongRunningIntent, SyncableEntity
- Human Interface Guidelines: Materials - рекомендації з дизайну Liquid Glass
Чек-лист Розробки Інтерактивних Віджетів iOS
WidgetKit з App Intents перетворює віджети на справжні інтерактивні розширення застосунків iOS. Архітектура, представлена в iOS 17, залишається стабільною до iOS 26, з візуальними вдосконаленнями та функціями продуктивності, що додаються по ходу.
Чек-лист імплементації
- Налаштуйте App Group для обміну даними між застосунком та розширенням віджета
- Створіть Timeline Provider з відповідною політикою оновлення
- Реалізуйте App Intents для кожної дії користувача
- Використовуйте
Button(intent:)абоToggle(intent:)для інтерактивності - Викликайте
WidgetCenter.shared.reloadTimelinesпісля модифікації даних - Додайте обов'язковий
.containerBackgroundдля сумісності з iOS 17+ - Використовуйте
.widgetAccentedRenderingModeдля підтримки iOS 26 Liquid Glass - Реалізуйте плавні анімації переходу з
.contentTransition - Обробляйте стани завантаження та помилок для візуального зворотного зв'язку
- Використовуйте
EntityCollectionдля масових операцій, щоб уникнути накладних витрат на резолвінг - Тестуйте на всіх підтримуваних розмірах віджетів, включно з
.systemExtraLarge
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Чи знайдеш ти помилку в iOS?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 24 серпня 2026 р.
Теги
Поділитися
Пов'язані статті

App Intents 2.0 та Siri Shortcuts: посібник з автоматизації iOS 27
Повний посібник з App Intents 2.0 та Siri Shortcuts для iOS 27. Створення streaming responses, багатотурових розмов, View Annotations та інтеграція з Foundation Models.

Combine vs async/await у Swift: Шаблони Прогресивної Міграції
Повний посібник з міграції з Combine на async/await у Swift: прогресивні стратегії, шаблони мостування та співіснування парадигм у iOS-кодових базах.

Питання співбесід з доступності iOS у 2026: VoiceOver і Dynamic Type
Підготовка до співбесід з iOS із ключовими питаннями про доступність: VoiceOver, Dynamic Type, семантичні traits та аудити.