WidgetKit iOS 17-26: Widget แบบโต้ตอบด้วย App Intents

คู่มือฉบับสมบูรณ์ในการสร้าง iOS widget แบบโต้ตอบด้วย WidgetKit และ App Intents ปุ่ม สวิตช์ แอนิเมชัน material Liquid Glass iOS 26 และอัปเดต WWDC 2026

WidgetKit iOS 17+ พร้อม widget แบบโต้ตอบและ App Intents สำหรับแอปพลิเคชัน iOS สมัยใหม่

iOS 17 นำเสนอการโต้ตอบแบบ native ให้กับ WidgetKit ทำให้ widget สามารถตอบสนองต่อการกระทำของผู้ใช้ได้โดยตรงจากหน้าจอหลักโดยไม่ต้องเปิดแอป iOS 26 ปรับปรุงสิ่งนี้ด้วย material Liquid Glass และโหมด rendering แบบมีจุดเน้น คู่มือนี้ครอบคลุมการพัฒนา widget แบบโต้ตอบตั้งแต่ iOS 17 ถึง iOS 26

สิ่งที่บทความนี้ครอบคลุม

การสร้าง widget แบบโต้ตอบ iOS 17-26 ตั้งแต่การตั้งค่าโปรเจกต์ไปจนถึงรูปแบบขั้นสูงพร้อมแอนิเมชัน การจัดการสถานะ และฟีเจอร์ App Intents WWDC 2026 ล่าสุด

สถาปัตยกรรม Widget แบบโต้ตอบใน iOS 17+

ความสามารถในการโต้ตอบของ widget iOS 17+ ทำงานผ่าน framework App Intents ต่างจาก deep link แบบดั้งเดิมที่จะเปิดแอป App Intents อนุญาตให้รันโค้ดโดยตรงจาก widget จากนั้นรีเฟรชการแสดงผลโดยอัตโนมัติด้วยข้อมูลใหม่

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])
    }
}

Widget ประกาศการกำหนดค่าและระบุ provider ที่จะให้ข้อมูล แอตทริบิวต์ .containerBackground จำเป็นสำหรับ iOS 17 สำหรับ widget แบบโต้ตอบ และ iOS 26 จะใช้สไตล์ Liquid Glass โดยอัตโนมัติเมื่อเหมาะสม

การสร้าง Timeline Provider

Timeline Provider กำหนดเวลาและวิธีที่ widget รีเฟรช สำหรับ widget แบบโต้ตอบ ยังต้องตอบสนองต่อการเปลี่ยนแปลงที่เกิดจาก 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)
    }
}

Provider ใช้ TaskDataManager ที่ใช้ร่วมกันเพื่อเข้าถึงข้อมูล วิธีการนี้รับประกันการซิงโครไนซ์ระหว่างแอปหลักและ widget สำหรับรูปแบบการจัดการสถานะที่เกี่ยวข้อง ดู SwiftUI @Observable vs @State

App Group จำเป็น

ในการแชร์ข้อมูลระหว่างแอปและ widget ต้องกำหนดค่า App Group ใน capabilities ของโปรเจกต์ UserDefaults หรือไฟล์ต้องใช้กลุ่มที่ใช้ร่วมกันนี้

ตัวจัดการข้อมูลที่ใช้ร่วมกัน

การแชร์ข้อมูลระหว่างแอปและ widget ต้องการคอนเทนเนอร์ทั่วไปที่เข้าถึงได้ผ่าน 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 ของ widget

การสร้าง App Intent สำหรับการโต้ตอบ

App Intent กำหนดการกระทำที่ดำเนินการเมื่อผู้ใช้โต้ตอบกับ widget iOS ดำเนินการนี้ในเบื้องหลังแล้วรีเฟรช widget โดยอัตโนมัติ

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 ทำให้เกิดการรีเฟรช widget ทันทีหลังจากการกระทำ รับประกันการตอบสนองทางสายตาในทันที

พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

View ของ Widget พร้อมปุ่มโต้ตอบ

View ของ widget ใช้ส่วนประกอบ Button มาตรฐานของ SwiftUI พร้อม intent เป็นการกระทำ 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() แล้วรีเฟรช widget โดยอัตโนมัติ

Toggle แบบโต้ตอบสำหรับ Widget

สำหรับการกระทำประเภทเปิด/ปิด ส่วนประกอบ Toggle เป็นทางเลือกแทนปุ่มด้วยสไตล์ iOS native

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 อย่างเป็นธรรมชาติ

ข้อจำกัดของ widget แบบโต้ตอบ

Widget ไม่สามารถแสดง alert, sheet หรือการนำทาง การกระทำทั้งหมดต้องเป็นแบบอิสระและอัปเดตสถานะที่มองเห็นได้โดยตรง

แอนิเมชันการรีเฟรชและการเปลี่ยนผ่าน

iOS 17+ อนุญาตให้สร้างแอนิเมชันการเปลี่ยนผ่านระหว่างการรีเฟรช widget หลังจากการกระทำ Modifier .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 และ Rendering แบบมีจุดเน้น

iOS 26 นำเสนอ material Liquid Glass และโหมด rendering แบบมีจุดเน้นสำหรับ widget การอัปเดตทางสายตาเหล่านี้ถูกใช้โดยอัตโนมัติเมื่อใช้ .containerBackground แต่นักพัฒนาสามารถควบคุมพฤติกรรมการ rendering ได้

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

Modifier .widgetAccentedRenderingMode(.fullColor) รักษาสีภาพในสภาพแวดล้อมที่มีจุดเน้น Family .systemExtraLarge ซึ่งเดิมมาจาก visionOS 26 ตอนนี้พร้อมใช้งานบน iOS และ iPadOS

WWDC 2026 App Intents: EntityCollection สำหรับประสิทธิภาพ

WWDC 2026 นำเสนอ EntityCollection เพื่อจัดการ entity จำนวนมากอย่างมีประสิทธิภาพ แทนที่จะ resolve entity ทั้งหมดก่อนการดำเนินการ intent จะส่งผ่านเฉพาะ identifier เท่านั้น

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

รูปแบบนี้มีความสำคัญสำหรับการดำเนินการแบบกลุ่ม Benchmark ภายในของ Apple แสดงให้เห็นว่าการติดแท็กรูปภาพ 1000 รูปเปลี่ยนจากช้าเป็น "แทบจะทันที" ด้วย EntityCollection

Long-Running Intents พร้อมแสดง Progress

WWDC 2026 ยังนำเสนอ LongRunningIntent สำหรับการดำเนินการที่เกินขีดจำกัด 30 วินาทีมาตรฐาน โดยแสดง progress เป็น 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)")
    }
}

Progress ปรากฏเป็น Live Activity และ CancellableIntent ให้เหตุผลการยกเลิกเพื่อให้สามารถจัดการ cleanup ได้อย่างเหมาะสม สำหรับข้อมูลเพิ่มเติมเกี่ยวกับรูปแบบ concurrency ของ Swift ดู Swift Structured Concurrency

Widget ที่กำหนดค่าได้ด้วย AppIntentConfiguration

สำหรับ widget ที่ปรับแต่งโดยผู้ใช้ได้ (ตัวกรอง หมวดหมู่) 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])
    }
}

ผู้ใช้สามารถกำหนดค่า widget ผ่านการกดค้างได้แล้ว มอบประสบการณ์ที่ปรับแต่งได้โดยไม่ต้องมีโค้ดเพิ่มเติมในแอป สำหรับข้อมูลเพิ่มเติมเกี่ยวกับ App Intents และการรวม Siri ดู App Intents 2.0 และ Siri Shortcuts

แนวปฏิบัติที่ดีที่สุดและการเพิ่มประสิทธิภาพ

รูปแบบหลายรูปแบบรับรองว่า widget แบบโต้ตอบจะมีประสิทธิภาพและเชื่อถือได้

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
    }
}

การเพิ่มประสิทธิภาพเหล่านี้รับประกัน widget ที่ตอบสนองได้ดีโดยไม่ใช้แบตเตอรี่มากเกินไป

การดีบัก widget

ใช้ scheme ของ widget ใน Xcode สำหรับการดีบัก ตัวอย่าง canvas อนุญาตให้ทดสอบขนาดและสถานะต่าง ๆ โดยไม่ต้องติดตั้งบนอุปกรณ์

Sources

Checklist การพัฒนา Widget iOS แบบโต้ตอบ

WidgetKit พร้อม App Intents เปลี่ยน widget ให้เป็นส่วนขยายแบบโต้ตอบที่แท้จริงของแอป iOS สถาปัตยกรรมที่นำเสนอใน iOS 17 ยังคงเสถียรจนถึง iOS 26 โดยมีการปรับปรุงทางสายตาและฟีเจอร์ประสิทธิภาพเพิ่มเติมตลอดทาง

Checklist การ implement

  • กำหนดค่า App Group สำหรับการแชร์ข้อมูลระหว่างแอปและ widget extension
  • สร้าง Timeline Provider พร้อมนโยบายการรีเฟรชที่เหมาะสม
  • implement App Intents สำหรับแต่ละการกระทำของผู้ใช้
  • ใช้ Button(intent:) หรือ Toggle(intent:) สำหรับการโต้ตอบ
  • เรียก WidgetCenter.shared.reloadTimelines หลังจากแก้ไขข้อมูล
  • เพิ่ม .containerBackground ที่จำเป็นสำหรับความเข้ากันได้ iOS 17+
  • ใช้ .widgetAccentedRenderingMode เพื่อรองรับ Liquid Glass iOS 26
  • implement แอนิเมชันการเปลี่ยนผ่านที่ลื่นไหลด้วย .contentTransition
  • จัดการสถานะการโหลดและข้อผิดพลาดสำหรับการตอบสนองทางสายตา
  • ใช้ EntityCollection สำหรับการดำเนินการแบบกลุ่มเพื่อหลีกเลี่ยง overhead ในการ resolve
  • ทดสอบบนขนาด widget ที่รองรับทั้งหมดรวมถึง .systemExtraLarge

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน iOS เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 24 สิงหาคม 2569

แท็ก

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

แชร์

บทความที่เกี่ยวข้อง

App Intents และ Siri Shortcuts สำหรับระบบอัตโนมัติ iOS ขั้นสูงด้วย Swift และ Apple Intelligence

App Intents 2.0 และ Siri Shortcuts: คู่มือระบบอัตโนมัติ iOS 27

คู่มือฉบับสมบูรณ์เกี่ยวกับ App Intents 2.0 และ Siri Shortcuts สำหรับ iOS 27 สร้าง streaming responses, การสนทนา multi-turn, View Annotations และรวมกับ Foundation Models

งาน iOS Developer ปี 2026

งาน iOS Developer ปี 2026: แหล่งหางาน ช่วงเงินเดือน และการเตรียมตัวสัมภาษณ์

คู่มือครบถ้วนสำหรับการหางาน iOS Developer ในปี 2026 ค้นพบแหล่งหางานที่ดีที่สุด ข้อมูลเงินเดือนล่าสุด และกลยุทธ์การเตรียมตัวสัมภาษณ์เทคนิค

การย้ายจาก Combine ไปยัง async/await ใน Swift พร้อมรูปแบบการอยู่ร่วมกัน

Combine vs async/await ใน Swift: รูปแบบการย้ายระบบแบบค่อยเป็นค่อยไป

คู่มือฉบับสมบูรณ์สำหรับการย้ายจาก Combine ไปยัง async/await ใน Swift: กลยุทธ์แบบค่อยเป็นค่อยไป รูปแบบการเชื่อมโยง และการอยู่ร่วมกันของกระบวนทัศน์ในโค้ดเบส iOS