WidgetKit iOS 17-26: App Intentsによるインタラクティブウィジェット構築

WidgetKitとApp IntentsでインタラクティブなiOSウィジェットを作成する完全ガイド。ボタン、トグル、アニメーション、iOS 26 Liquid Glassマテリアル、WWDC 2026アップデートを解説。

モダンなiOSアプリケーション向けインタラクティブウィジェットとApp Intentsを備えたWidgetKit iOS 17+

iOS 17はWidgetKitにネイティブなインタラクティブ機能を導入し、アプリを開かずにホーム画面から直接ユーザーの操作に応答できるようになりました。iOS 26ではLiquid Glassマテリアルとアクセント付きレンダリングモードによりこれが洗練されました。このガイドでは、iOS 17からiOS 26までのインタラクティブウィジェット開発を解説します。

この記事の内容

プロジェクト設定からアニメーションと状態管理を含む高度なパターン、最新のWWDC 2026 App Intents機能まで、iOS 17-26のインタラクティブウィジェット作成を解説します。

iOS 17+におけるインタラクティブウィジェットのアーキテクチャ

iOS 17+のウィジェットインタラクティブ機能は、App Intentsフレームワークを通じて動作します。アプリを開く従来のディープリンクとは異なり、App Intentsはウィジェットから直接コードを実行し、新しいデータで表示を自動的に更新します。

InteractiveWidgetArchitecture.swiftswift
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によってトリガーされた変更にも対応する必要があります。

TaskTimelineProvider.swiftswift
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 { /bin/bash }
        )
        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でApp Groupを設定する必要があります。UserDefaultsまたはファイルはこの共有グループを使用する必要があります。

共有データマネージャー

アプリケーションとウィジェット間のデータ共有には、App Group経由でアクセス可能な共通コンテナが必要です。

TaskDataManager.swiftswift
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: { /bin/bash.id == task.id }) {
            tasks[index] = task
            saveTasks(tasks)
        }
    }

    // Toggle completed state
    func toggleTaskCompletion(taskID: UUID) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { /bin/bash.id == taskID }) {
            tasks[index].isCompleted.toggle()
            saveTasks(tasks)
        }
    }
}

このマネージャーはすべての永続化ロジックをカプセル化し、アプリケーションとウィジェットのApp Intents両方で使用されます。

インタラクティブ機能のためのApp Intent作成

App Intentは、ユーザーがウィジェットと対話する際に実行されるアクションを定義します。iOSはこのアクションをバックグラウンドで実行し、その後ウィジェットを自動的に更新します。

ToggleTaskIntent.swiftswift
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は、intentをアクションとする標準のSwiftUI Buttonコンポーネントを使用します。iOS 17+はこれらのインタラクションを自動的にインターセプトしてApp Intentを実行します。

TaskWidgetView.swiftswift
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 { !/bin/bash.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スタイリングでボタンの代替を提供します。

ToggleWidgetView.swiftswift
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: { /bin/bash.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デザインに自然に統合されます。

インタラクティブウィジェットの制限

ウィジェットはアラート、シート、ナビゲーションを表示できません。すべてのアクションは自己完結型で、可視状態を直接更新する必要があります。

更新アニメーションとトランジション

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) {
                // 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を使用すると自動的に適用されますが、開発者はレンダリング動作を制御できます。

iOS26WidgetStyling.swiftswift
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)修飾子は、アクセント環境で画像の色を保持します。visionOS 26で最初に登場した.systemExtraLargeファミリーは、iOSとiPadOSでも利用可能になりました。

WWDC 2026 App Intents: パフォーマンスのためのEntityCollection

WWDC 2026では、多数のエンティティを効率的に処理するためのEntityCollectionが導入されました。Intent実行前にすべてのエンティティを解決する代わりに、識別子のみが渡されます。

EntityCollectionIntent.swiftswift
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: /bin/bash) })
    }
    
    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: { /bin/bash.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: /bin/bash) }
    }
}

このパターンは一括操作に不可欠です。Appleの内部ベンチマークでは、1000枚の写真にタグ付けする処理がEntityCollectionにより「ほぼ瞬時」になったことが示されました。

進捗表示付き長時間実行Intent

WWDC 2026では、標準の30秒制限を超える操作のためのLongRunningIntentも導入され、進捗はLive Activityとして表示されます。

LongRunningTaskIntent.swiftswift
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を置き換えます。

ConfigurableTaskWidget.swiftswift
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 { /bin/bash.priority == priority }
        }

        // Filter completed if needed
        if !config.showCompleted {
            tasks = tasks.filter { !/bin/bash.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ショートカットを参照してください。

ベストプラクティスと最適化

いくつかのパターンがパフォーマンスと信頼性の高いインタラクティブウィジェットを保証します。

WidgetBestPractices.swiftswift
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 { !/bin/bash.isCompleted }.count
        self.highPriorityCount = tasks.filter { /bin/bash.priority == .high && !/bin/bash.isCompleted }.count
    }
}

これらの最適化により、過度にバッテリーを消費しないレスポンシブなウィジェットが保証されます。

ウィジェットのデバッグ

デバッグにはXcodeのウィジェットスキームを使用してください。キャンバスプレビューにより、デバイスにインストールせずにさまざまなサイズと状態をテストできます。

Sources

インタラクティブiOSウィジェット開発チェックリスト

App Intents付きWidgetKitは、ウィジェットをiOSアプリケーションの真のインタラクティブな拡張機能に変えます。iOS 17で導入されたアーキテクチャはiOS 26まで安定しており、視覚的な改善とパフォーマンス機能が追加されています。

実装チェックリスト

  • アプリとウィジェット拡張機能間のデータ共有用にApp Groupを設定
  • 適切な更新ポリシーを持つTimeline Providerを作成
  • 各ユーザーアクション用のApp Intentsを実装
  • インタラクティブ機能にはButton(intent:)またはToggle(intent:)を使用
  • データ変更後にWidgetCenter.shared.reloadTimelinesを呼び出す
  • iOS 17+互換性のために必須の.containerBackgroundを追加
  • iOS 26 Liquid Glassサポートのために.widgetAccentedRenderingModeを使用
  • .contentTransitionでスムーズなトランジションアニメーションを実装
  • 視覚的フィードバックのためのロード状態とエラー状態を処理
  • 一括操作で解決オーバーヘッドを避けるためにEntityCollectionを使用
  • .systemExtraLargeを含むすべてのサポートされているウィジェットサイズでテスト

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

今日のチャレンジ

iOS のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年8月24日 更新

タグ

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

共有

関連記事