WidgetKit iOS 17+: Інтерактивні Віджети з App Intents

Повний посібник зі створення інтерактивних iOS-віджетів з WidgetKit та App Intents. Кнопки, перемикачі, анімації та найкращі практики для iOS 17+ у 2026 році.

WidgetKit iOS 17+ з інтерактивними віджетами та App Intents для сучасних iOS-застосунків

iOS 17 здійснив революцію у WidgetKit, запровадивши нативну інтерактивність. Віджети більше не є статичними відображеннями: тепер вони можуть реагувати на дії користувача безпосередньо з головного екрана, не відкриваючи застосунок. Ця важлива еволюція ґрунтується на фреймворку App Intents і пропонує плавний, сучасний користувацький досвід.

Що охоплює ця стаття

Ця стаття представляє повне створення інтерактивних віджетів iOS 17+, від конфігурації проєкту до просунутих патернів з анімаціями та керуванням станом.

Архітектура Інтерактивних Віджетів

Інтерактивність віджетів iOS 17+ працює через фреймворк App Intents. На відміну від традиційних deep-посилань, які відкривали б застосунок, App Intents дозволяють виконувати код безпосередньо з віджета, після чого автоматично оновлюють відображення з новими даними.

InteractiveWidgetArchitecture.swiftswift
import WidgetKit
import SwiftUI
import AppIntents

// Архітектура базується на трьох головних компонентах:
// 1. Widget Timeline Provider - постачає дані
// 2. Widget View - відображає інтерфейс з Button/Toggle
// 3. App Intent - виконує дію при дотику

struct TaskWidget: Widget {
    // Унікальний ідентифікатор віджета
    let kind: String = "TaskWidget"

    var body: some WidgetConfiguration {
        // StaticConfiguration для віджетів без параметрів
        StaticConfiguration(
            kind: kind,
            provider: TaskTimelineProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
                // Обов'язково для App Intents
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Завдання")
        .description("Керуйте своїми завданнями з головного екрана.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

Віджет оголошує свою конфігурацію та вказує постачальника, який надаватиме дані. Атрибут .containerBackground обов'язковий з iOS 17 для інтерактивних віджетів.

Створення Timeline Provider

Timeline Provider визначає, коли і як віджет оновлюється. Для інтерактивних віджетів він також повинен реагувати на зміни, спричинені App Intents.

TaskTimelineProvider.swiftswift
import WidgetKit
import SwiftUI

// Entry, що представляє стан віджета у визначений момент
struct TaskEntry: TimelineEntry {
    let date: Date
    let tasks: [Task]

    // Стан завантаження для візуального зворотного зв'язку
    var isLoading: Bool = false
}

// Модель даних, спільна між застосунком і віджетом
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, що відображається під час початкового завантаження
    func placeholder(in context: Context) -> TaskEntry {
        TaskEntry(
            date: Date(),
            tasks: [
                Task(id: UUID(), title: "Приклад завдання", isCompleted: false, priority: .medium)
            ]
        )
    }

    // Snapshot для галереї віджетів
    func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
        let entry = TaskEntry(
            date: Date(),
            tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { $0 }
        )
        completion(entry)
    }

    // Повна timeline з політикою оновлення
    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)))

        // Оновлення через 15 хвилин або після дії користувача
        let nextUpdate = Calendar.current.date(
            byAdding: .minute,
            value: 15,
            to: Date()
        ) ?? Date()

        let timeline = Timeline(
            entries: [entry],
            policy: .after(nextUpdate)
        )
        completion(timeline)
    }
}

Постачальник використовує спільний TaskDataManager для доступу до даних. Цей підхід гарантує синхронізацію між головним застосунком і віджетом.

App Group обов'язковий

Для обміну даними між застосунком і віджетом потрібно налаштувати App Group у capabilities проєкту. UserDefaults або файли повинні використовувати цю спільну групу.

Менеджер Спільних Даних

Обмін даними між застосунком і віджетом вимагає спільного контейнера, доступного через App Group.

TaskDataManager.swiftswift
import Foundation

final class TaskDataManager {
    // Singleton для глобального доступу
    static let shared = TaskDataManager()

    // Ідентифікатор App Group, налаштований у Xcode
    private let appGroupID = "group.com.example.taskapp"

    // UserDefaults, спільний між застосунком і віджетом
    private var sharedDefaults: UserDefaults? {
        UserDefaults(suiteName: appGroupID)
    }

    private let tasksKey = "tasks"

    private init() {}

    // Отримує завдання зі спільного сховища
    func fetchTasks() -> [Task] {
        guard let data = sharedDefaults?.data(forKey: tasksKey),
              let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
            return []
        }
        return tasks
    }

    // Зберігає зі сповіщенням віджета
    func saveTasks(_ tasks: [Task]) {
        guard let data = try? JSONEncoder().encode(tasks) else { return }
        sharedDefaults?.set(data, forKey: tasksKey)
    }

    // Оновлює конкретне завдання
    func updateTask(_ task: Task) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks[index] = task
            saveTasks(tasks)
        }
    }

    // Перемикає стан виконання
    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 виконує цю дію у фоновому режимі, а потім автоматично оновлює віджет.

ToggleTaskIntent.swiftswift
import AppIntents
import WidgetKit

// Intent для перемикання стану завдання
struct ToggleTaskIntent: AppIntent {
    // Заголовок, що відображається у швидких командах Siri
    static var title: LocalizedStringResource = "Перемкнути стан завдання"

    // Опис для доступності
    static var description = IntentDescription("Позначає завдання як виконане або невиконане.")

    // Параметр: ID завдання для зміни
    @Parameter(title: "ID завдання")
    var taskID: String

    // Обов'язковий ініціалізатор для AppIntent
    init() {}

    // Ініціалізатор з параметром для створення з view
    init(taskID: UUID) {
        self.taskID = taskID.uuidString
    }

    // Виконання дії
    func perform() async throws -> some IntentResult {
        // Конвертація рядкового ID в UUID
        guard let uuid = UUID(uuidString: taskID) else {
            return .result()
        }

        // Оновлення завдання
        TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)

        // Запит на оновлення віджета
        WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")

        return .result()
    }
}

Виклик WidgetCenter.shared.reloadTimelines спричиняє негайне оновлення віджета після дії, гарантуючи миттєвий візуальний зворотний зв'язок.

Готовий до співбесід з iOS?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

View Віджета з Інтерактивними Кнопками

View віджета використовує стандартний компонент SwiftUI Button з intent як дію. iOS 17+ автоматично перехоплює ці взаємодії для виконання App Intent.

TaskWidgetView.swiftswift
import SwiftUI
import WidgetKit

struct TaskWidgetView: View {
    let entry: TaskEntry

    // Адаптація до розміру віджета
    @Environment(\.widgetFamily) var family

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // Заголовок з назвою та лічильником
            headerView

            // Список завдань з інтерактивними кнопками
            ForEach(entry.tasks.prefix(tasksLimit)) { task in
                TaskRowView(task: task)
            }

            Spacer(minLength: 0)
        }
        .padding()
    }

    // Кількість завдань залежно від розміру
    private var tasksLimit: Int {
        switch family {
        case .systemSmall: return 2
        case .systemMedium: return 3
        default: return 4
        }
    }

    private var headerView: some View {
        HStack {
            Text("Завдання")
                .font(.headline)
                .fontWeight(.bold)

            Spacer()

            // Бейдж з кількістю завдань, що залишилися
            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 {
        // Кнопка з App Intent як дією
        Button(intent: ToggleTaskIntent(taskID: task.id)) {
            HStack(spacing: 12) {
                // Індикатор виконання
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title3)
                    .foregroundStyle(task.isCompleted ? .green : .secondary)

                // Назва завдання
                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
                    .lineLimit(1)

                Spacer()

                // Індикатор пріоритету
                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.

ToggleWidgetView.swiftswift
import SwiftUI
import AppIntents

// Специфічний intent для Toggle з явним станом
struct SetTaskCompletionIntent: AppIntent {
    static var title: LocalizedStringResource = "Встановити стан завдання"

    @Parameter(title: "ID завдання")
    var taskID: String

    // Цільовий стан: true = виконано, false = не виконано
    @Parameter(title: "Виконано")
    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 }) {
            // Встановлює стан явно (не 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()

            // Інтерактивний toggle з 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 керує цими анімаціями.

AnimatedTaskWidgetView.swiftswift
import SwiftUI
import WidgetKit

struct AnimatedTaskRowView: View {
    let task: Task

    var body: some View {
        Button(intent: ToggleTaskIntent(taskID: task.id)) {
            HStack(spacing: 12) {
                // Іконка з анімацією переходу
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title3)
                    .foregroundStyle(task.isCompleted ? .green : .secondary)
                    // Анімація іконки при зміні
                    .contentTransition(.symbolEffect(.replace))

                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
                    // Анімація тексту
                    .contentTransition(.opacity)

                Spacer()
            }
            .padding(.vertical, 6)
            .padding(.horizontal, 10)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear)
            )
            // Анімація фону
            .animation(.easeInOut(duration: 0.3), value: task.isCompleted)
        }
        .buttonStyle(.plain)
    }
}

// Віджет з анімованою інвалідацією
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("Анімовані Завдання")
        .description("Віджети з плавними анімаціями.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
        // Активація анімацій вмісту
        .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("Завдання")
                .font(.headline.bold())

            Spacer()

            let completed = entry.tasks.filter(\.isCompleted).count
            let total = entry.tasks.count

            // Анімований прогрес
            Text("\(completed)/\(total)")
                .font(.caption.bold())
                .foregroundStyle(.secondary)
                .contentTransition(.numericText())
        }
    }
}

Анімації .symbolEffect(.replace) та .numericText() створюють плавні переходи між станами, значно покращуючи користувацький досвід.

Налаштовуваний Віджет з AppIntentConfiguration

Для віджетів, що налаштовуються користувачем (фільтри, категорії), AppIntentConfiguration замінює StaticConfiguration.

ConfigurableTaskWidget.swiftswift
import WidgetKit
import SwiftUI
import AppIntents

// Конфігурація, доступна користувачу
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Конфігурація завдань"
    static var description = IntentDescription("Налаштуйте відображення завдань.")

    // Фільтр за пріоритетом
    @Parameter(title: "Пріоритет", default: .all)
    var priorityFilter: PriorityFilter

    // Показати виконані завдання
    @Parameter(title: "Показати виконані", default: true)
    var showCompleted: Bool

    // Максимальна кількість завдань
    @Parameter(title: "Кількість завдань", default: 3)
    var maxTasks: Int
}

// Enum для фільтра пріоритету
enum PriorityFilter: String, AppEnum {
    case all
    case high
    case medium
    case low

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Пріоритет"

    static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
        .all: "Усі",
        .high: "Високий",
        .medium: "Середній",
        .low: "Низький"
    ]
}

// Постачальник, адаптований до конфігурації
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))
    }

    // Застосовує фільтри конфігурації
    private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
        var tasks = TaskDataManager.shared.fetchTasks()

        // Фільтрація за пріоритетом
        if config.priorityFilter != .all {
            let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
            tasks = tasks.filter { $0.priority == priority }
        }

        // Фільтрація виконаних за необхідності
        if !config.showCompleted {
            tasks = tasks.filter { !$0.isCompleted }
        }

        // Обмеження кількості
        return Array(tasks.prefix(config.maxTasks))
    }
}

// Віджет з конфігурацією користувача
struct ConfigurableTaskWidget: Widget {
    let kind: String = "ConfigurableTaskWidget"

    var body: some WidgetConfiguration {
        // AppIntentConfiguration для налаштовуваних віджетів
        AppIntentConfiguration(
            kind: kind,
            intent: TaskWidgetConfigurationIntent.self,
            provider: ConfigurableTaskProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Персоналізовані Завдання")
        .description("Фільтруйте та персоналізуйте свої завдання.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

Користувач тепер може налаштувати віджет довгим натисканням, отримуючи персоналізований досвід без додаткового коду в застосунку.

Обробка Помилок та Стани Завантаження

Хороший UX вимагає обробки випадків помилок та проміжних станів під час взаємодії.

TaskIntentWithFeedback.swiftswift
import AppIntents
import WidgetKit

struct ToggleTaskWithFeedbackIntent: AppIntent {
    static var title: LocalizedStringResource = "Перемкнути завдання зі зворотним зв'язком"

    @Parameter(title: "ID завдання")
    var taskID: String

    init() {}

    init(taskID: UUID) {
        self.taskID = taskID.uuidString
    }

    func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
        guard let uuid = UUID(uuidString: taskID) else {
            // Повертає тиху помилку
            return .result(value: false)
        }

        // Симулює асинхронну операцію (наприклад, синхронізація з сервером)
        do {
            try await Task.sleep(for: .milliseconds(100))

            TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
            WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")

            return .result(value: true)
        } catch {
            // Помилка: не оновлювати віджет
            return .result(value: false)
        }
    }
}

// View зі станом завантаження
struct TaskRowWithLoadingView: View {
    let task: Task
    @State private var isLoading = false

    var body: some View {
        Button(intent: ToggleTaskIntent(taskID: task.id)) {
            HStack(spacing: 12) {
                // Умовний індикатор
                Group {
                    if isLoading {
                        ProgressView()
                            .scaleEffect(0.8)
                    } else {
                        Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                            .foregroundStyle(task.isCompleted ? .green : .secondary)
                    }
                }
                .frame(width: 24, height: 24)

                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)

                Spacer()
            }
            .padding(.vertical, 6)
            .padding(.horizontal, 10)
            .background(Color(.systemBackground).opacity(0.5))
            .cornerRadius(8)
        }
        .buttonStyle(.plain)
        .disabled(isLoading)
        .opacity(isLoading ? 0.6 : 1.0)
    }
}

Негайний візуальний зворотний зв'язок (зменшена прозорість, індикатор завантаження) повідомляє користувачу, що його дія зареєстрована.

Найкращі Практики та Оптимізації

Кілька патернів забезпечують продуктивні та надійні інтерактивні віджети.

WidgetBestPractices.swiftswift
import WidgetKit
import SwiftUI

// 1. Завжди інвалідувати кеш після модифікації
final class WidgetRefreshManager {
    static func refreshAllWidgets() {
        // Оновлення всіх віджетів застосунку
        WidgetCenter.shared.reloadAllTimelines()
    }

    static func refreshWidget(kind: String) {
        // Оновлення конкретного віджета
        WidgetCenter.shared.reloadTimelines(ofKind: kind)
    }

    // Виклик з застосунку після модифікації даних
    static func notifyDataChanged() {
        Task { @MainActor in
            refreshAllWidgets()
        }
    }
}

// 2. Обмежити складність view
struct OptimizedWidgetView: View {
    let entry: TaskEntry

    var body: some View {
        // Надавати перевагу простим view без GeometryReader
        VStack(alignment: .leading, spacing: 8) {
            ForEach(entry.tasks.prefix(3)) { task in
                // Легкі компоненти
                minimalTaskRow(task)
            }
        }
        .padding()
    }

    // Мінімальна view для продуктивності
    @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. Використовувати @AppStorage для простих станів
struct QuickSettingsWidgetView: View {
    // Прямий доступ до спільних UserDefaults
    @AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
    private var showCompleted = true

    var body: some View {
        // Стан зберігається між оновленнями
        Text(showCompleted ? "Показую всі" : "Приховую виконані")
    }
}

// 4. Попередньо обчислювати дані в постачальнику
struct OptimizedTaskEntry: TimelineEntry {
    let date: Date
    let tasks: [Task]

    // Попередньо обчислені дані
    let completedCount: Int
    let pendingCount: Int
    let highPriorityCount: Int

    init(date: Date, tasks: [Task]) {
        self.date = date
        self.tasks = tasks

        // Обчислення виконуються один раз
        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 iOS 17+ з App Intents перетворює віджети на справжні інтерактивні розширення застосунків iOS. Ця декларативна архітектура значно спрощує розробку, пропонуючи нативний та плавний користувацький досвід.

Чек-лист Інтерактивного Віджета iOS 17+

  • ✅ Налаштувати App Group для обміну даними
  • ✅ Створити Timeline Provider з відповідним оновленням
  • ✅ Реалізувати App Intents для кожної дії
  • ✅ Використовувати Button(intent:) або Toggle(intent:) для інтерактивності
  • ✅ Викликати WidgetCenter.shared.reloadTimelines після модифікації
  • ✅ Додати обов'язковий .containerBackground для iOS 17+
  • ✅ Реалізувати плавні анімації переходу
  • ✅ Обробляти стани завантаження та помилок
  • ✅ Оптимізувати view для продуктивності батареї
  • ✅ Тестувати на всіх підтримуваних розмірах віджетів

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#widgetkit
#ios
#app-intents
#swift
#widgets

Поділитися

Пов'язані статті