# WidgetKit iOS 17+: Widget แบบโต้ตอบด้วย App Intents > คู่มือฉบับสมบูรณ์ในการสร้าง iOS widget แบบโต้ตอบด้วย WidgetKit และ App Intents ปุ่ม สวิตช์ แอนิเมชัน และแนวปฏิบัติที่ดีที่สุดสำหรับ iOS 17+ ในปี 2026 - Published: 2026-03-04 - Updated: 2026-04-29 - Author: SharpSkill - Tags: widgetkit, ios, app-intents, swift, widgets - Reading time: 14 min --- iOS 17 ได้ปฏิวัติ WidgetKit โดยนำเสนอการโต้ตอบแบบ native วิดเจ็ตไม่ได้เป็นเพียงการแสดงผลแบบคงที่อีกต่อไป: ตอนนี้สามารถตอบสนองต่อการกระทำของผู้ใช้ได้โดยตรงจากหน้าจอหลัก โดยไม่ต้องเปิดแอปพลิเคชัน วิวัฒนาการครั้งสำคัญนี้พึ่งพาเฟรมเวิร์ก App Intents และมอบประสบการณ์การใช้งานที่ลื่นไหลและทันสมัย > **สิ่งที่บทความนี้ครอบคลุม** > > บทความนี้นำเสนอการสร้างวิดเจ็ตแบบโต้ตอบ iOS 17+ ที่สมบูรณ์ ตั้งแต่การกำหนดค่าโปรเจกต์ไปจนถึงรูปแบบขั้นสูงพร้อมแอนิเมชันและการจัดการสถานะ ## สถาปัตยกรรมของ Widget แบบโต้ตอบ ความสามารถในการโต้ตอบของ widget iOS 17+ ทำงานผ่านเฟรมเวิร์ก App Intents ต่างจาก deep link แบบดั้งเดิมที่จะเปิดแอปพลิเคชัน App Intents อนุญาตให้รันโค้ดโดยตรงจาก widget จากนั้นรีเฟรชการแสดงผลโดยอัตโนมัติด้วยข้อมูลใหม่ ```swift // InteractiveWidgetArchitecture.swift import WidgetKit import SwiftUI import AppIntents // สถาปัตยกรรมประกอบด้วยสามส่วนหลัก: // 1. Widget Timeline Provider - ให้ข้อมูล // 2. Widget View - แสดงอินเทอร์เฟซด้วย Button/Toggle // 3. App Intent - ดำเนินการเมื่อแตะ struct TaskWidget: Widget { // ตัวระบุที่ไม่ซ้ำกันของ widget let kind: String = "TaskWidget" var body: some WidgetConfiguration { // StaticConfiguration สำหรับ widget ที่ไม่มีพารามิเตอร์ StaticConfiguration( kind: kind, provider: TaskTimelineProvider() ) { entry in TaskWidgetView(entry: entry) // จำเป็นสำหรับ App Intents .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("งาน") .description("จัดการงานของคุณจากหน้าจอหลัก") .supportedFamilies([.systemSmall, .systemMedium]) } } ``` วิดเจ็ตประกาศการกำหนดค่าและระบุ provider ที่จะให้ข้อมูล แอตทริบิวต์ `.containerBackground` จำเป็นสำหรับ iOS 17 สำหรับ widget แบบโต้ตอบ ## การสร้าง Timeline Provider Timeline Provider กำหนดเวลาและวิธีที่ widget รีเฟรช สำหรับ widget แบบโต้ตอบ ยังต้องตอบสนองต่อการเปลี่ยนแปลงที่เกิดจาก App Intents ด้วย ```swift // TaskTimelineProvider.swift import WidgetKit import SwiftUI // Entry ที่แทนสถานะของ widget ในช่วงเวลาหนึ่ง struct TaskEntry: TimelineEntry { let date: Date let tasks: [Task] // สถานะการโหลดสำหรับการตอบสนองทางสายตา var isLoading: Bool = false } // แบบจำลองข้อมูลที่ใช้ร่วมกันระหว่างแอปและ 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 ที่แสดงระหว่างการโหลดเริ่มต้น func placeholder(in context: Context) -> TaskEntry { TaskEntry( date: Date(), tasks: [ Task(id: UUID(), title: "งานตัวอย่าง", isCompleted: false, priority: .medium) ] ) } // Snapshot สำหรับแกลเลอรี widget 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) -> 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) } } ``` Provider ใช้ `TaskDataManager` ที่ใช้ร่วมกันเพื่อเข้าถึงข้อมูล วิธีการนี้รับประกันการซิงโครไนซ์ระหว่างแอปพลิเคชันหลักและ widget > **App Group จำเป็น** > > ในการแชร์ข้อมูลระหว่างแอปและ widget ต้องกำหนดค่า App Group ใน capabilities ของโปรเจกต์ UserDefaults หรือไฟล์ต้องใช้กลุ่มที่ใช้ร่วมกันนี้ ## ตัวจัดการข้อมูลที่ใช้ร่วมกัน การแชร์ข้อมูลระหว่างแอปพลิเคชันและ widget ต้องการคอนเทนเนอร์ทั่วไปที่เข้าถึงได้ผ่าน App Group ```swift // TaskDataManager.swift import Foundation final class TaskDataManager { // Singleton สำหรับการเข้าถึงระดับโลก static let shared = TaskDataManager() // ตัวระบุ App Group ที่กำหนดค่าใน Xcode private let appGroupID = "group.com.example.taskapp" // UserDefaults ที่ใช้ร่วมกันระหว่างแอปและ widget 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 } // บันทึกพร้อมแจ้งเตือน widget 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 ของ widget ## การสร้าง App Intent สำหรับการโต้ตอบ App Intent กำหนดการกระทำที่ดำเนินการเมื่อผู้ใช้โต้ตอบกับ widget iOS ดำเนินการนี้ในเบื้องหลังแล้วรีเฟรช widget โดยอัตโนมัติ ```swift // ToggleTaskIntent.swift import AppIntents import WidgetKit // Intent สำหรับสลับสถานะของงาน struct ToggleTaskIntent: AppIntent { // ชื่อที่แสดงในทางลัด Siri static var title: LocalizedStringResource = "สลับสถานะของงาน" // คำอธิบายสำหรับการเข้าถึง static var description = IntentDescription("ทำเครื่องหมายงานว่าเสร็จสิ้นหรือยังไม่เสร็จสิ้น") // พารามิเตอร์: ID ของงานที่จะแก้ไข @Parameter(title: "ID งาน") var taskID: String // Initializer ที่จำเป็นสำหรับ AppIntent init() {} // Initializer พร้อมพารามิเตอร์สำหรับการสร้างจาก view init(taskID: UUID) { self.taskID = taskID.uuidString } // การดำเนินการ func perform() async throws -> some IntentResult { // แปลง string ID เป็น UUID guard let uuid = UUID(uuidString: taskID) else { return .result() } // อัปเดตงาน TaskDataManager.shared.toggleTaskCompletion(taskID: uuid) // ขอให้รีเฟรช widget WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget") return .result() } } ``` การเรียก `WidgetCenter.shared.reloadTimelines` ทำให้เกิดการรีเฟรช widget ทันทีหลังจากการกระทำ รับประกันการตอบสนองทางสายตาในทันที ## View ของ Widget พร้อมปุ่มโต้ตอบ View ของ widget ใช้ส่วนประกอบ `Button` มาตรฐานของ SwiftUI พร้อม intent เป็นการกระทำ iOS 17+ จับการโต้ตอบเหล่านี้โดยอัตโนมัติเพื่อดำเนินการ App Intent ```swift // TaskWidgetView.swift import SwiftUI import WidgetKit struct TaskWidgetView: View { let entry: TaskEntry // ปรับให้เข้ากับขนาดของ widget @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() // Badge พร้อมจำนวนงานที่เหลือ 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()` แล้วรีเฟรช widget โดยอัตโนมัติ ## Toggle แบบโต้ตอบสำหรับ Widget สำหรับการกระทำประเภทเปิด/ปิด ส่วนประกอบ `Toggle` เป็นทางเลือกแทนปุ่มด้วยสไตล์ iOS native ```swift // ToggleWidgetView.swift 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 อย่างเป็นธรรมชาติ > **ข้อจำกัดของ widget แบบโต้ตอบ** > > Widget ไม่สามารถแสดง alert, sheet หรือการนำทาง การกระทำทั้งหมดต้องเป็นแบบอิสระและอัปเดตสถานะที่มองเห็นได้โดยตรง ## แอนิเมชันการรีเฟรชและการเปลี่ยนผ่าน iOS 17+ อนุญาตให้สร้างแอนิเมชันการเปลี่ยนผ่านระหว่างการรีเฟรช widget หลังจากการกระทำ Modifier `.contentTransition` ควบคุมแอนิเมชันเหล่านี้ ```swift // AnimatedTaskWidgetView.swift 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) } } // Widget พร้อมการลบล้างแบบมีแอนิเมชัน 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("Widget พร้อมแอนิเมชันที่ลื่นไหล") .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()` สร้างการเปลี่ยนผ่านที่ลื่นไหลระหว่างสถานะ ปรับปรุงประสบการณ์ผู้ใช้อย่างมีนัยสำคัญ ## Widget ที่กำหนดค่าได้ด้วย AppIntentConfiguration สำหรับ widget ที่ปรับแต่งโดยผู้ใช้ได้ (ตัวกรอง หมวดหมู่) AppIntentConfiguration แทนที่ StaticConfiguration ```swift // ConfigurableTaskWidget.swift 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: "ต่ำ" ] } // Provider ที่ปรับให้เข้ากับการกำหนดค่า 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 { 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)) } } // Widget พร้อมการกำหนดค่าผู้ใช้ struct ConfigurableTaskWidget: Widget { let kind: String = "ConfigurableTaskWidget" var body: some WidgetConfiguration { // AppIntentConfiguration สำหรับ widget ที่กำหนดค่าได้ AppIntentConfiguration( kind: kind, intent: TaskWidgetConfigurationIntent.self, provider: ConfigurableTaskProvider() ) { entry in TaskWidgetView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("งานปรับแต่ง") .description("กรองและปรับแต่งงานของคุณ") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } } ``` ผู้ใช้สามารถกำหนดค่า widget ผ่านการกดค้างได้แล้ว มอบประสบการณ์ที่ปรับแต่งได้โดยไม่ต้องมีโค้ดเพิ่มเติมในแอปพลิเคชัน ## การจัดการข้อผิดพลาดและสถานะการโหลด UX ที่ดีต้องการการจัดการกรณีข้อผิดพลาดและสถานะระดับกลางระหว่างการโต้ตอบ ```swift // TaskIntentWithFeedback.swift 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 { guard let uuid = UUID(uuidString: taskID) else { // ส่งคืนความล้มเหลวแบบเงียบ return .result(value: false) } // จำลองการดำเนินการแบบ async (เช่น sync เซิร์ฟเวอร์) do { try await Task.sleep(for: .milliseconds(100)) TaskDataManager.shared.toggleTaskCompletion(taskID: uuid) WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget") return .result(value: true) } catch { // ข้อผิดพลาด: ไม่อัปเดต widget 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) } } ``` การตอบสนองทางสายตาทันที (ความทึบลดลง ตัวบ่งชี้การโหลด) แจ้งให้ผู้ใช้ทราบว่าการกระทำของพวกเขาได้รับการลงทะเบียนแล้ว ## แนวปฏิบัติที่ดีที่สุดและการเพิ่มประสิทธิภาพ รูปแบบหลายรูปแบบรับรองว่า widget แบบโต้ตอบจะมีประสิทธิภาพและเชื่อถือได้ ```swift // WidgetBestPractices.swift import WidgetKit import SwiftUI // 1. ทำให้ cache ใช้ไม่ได้เสมอหลังจากแก้ไข final class WidgetRefreshManager { static func refreshAllWidgets() { // รีเฟรช widget ทั้งหมดของแอป WidgetCenter.shared.reloadAllTimelines() } static func refreshWidget(kind: String) { // รีเฟรช widget เฉพาะ 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. คำนวณข้อมูลล่วงหน้าใน provider 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 ที่ตอบสนองได้ดีโดยไม่ใช้แบตเตอรี่มากเกินไป > **การดีบัก widget** > > ใช้ scheme ของ widget ใน Xcode สำหรับการดีบัก ตัวอย่าง canvas อนุญาตให้ทดสอบขนาดและสถานะต่าง ๆ โดยไม่ต้องติดตั้งบนอุปกรณ์ ## สรุป WidgetKit iOS 17+ พร้อม App Intents เปลี่ยน widget ให้เป็นส่วนขยายแบบโต้ตอบที่แท้จริงของแอปพลิเคชัน iOS สถาปัตยกรรมแบบประกาศนี้ทำให้การพัฒนาง่ายขึ้นอย่างมากในขณะที่มอบประสบการณ์ผู้ใช้ที่เป็น native และลื่นไหล ### รายการตรวจสอบ Widget แบบโต้ตอบ iOS 17+ - ✅ กำหนดค่า App Group สำหรับการแชร์ข้อมูล - ✅ สร้าง Timeline Provider พร้อมการรีเฟรชที่เหมาะสม - ✅ ใช้งาน App Intents สำหรับแต่ละการกระทำ - ✅ ใช้ `Button(intent:)` หรือ `Toggle(intent:)` สำหรับการโต้ตอบ - ✅ เรียก `WidgetCenter.shared.reloadTimelines` หลังจากการแก้ไข - ✅ เพิ่ม `.containerBackground` ที่จำเป็นสำหรับ iOS 17+ - ✅ ใช้งานแอนิเมชันการเปลี่ยนผ่านที่ลื่นไหล - ✅ จัดการสถานะการโหลดและข้อผิดพลาด - ✅ เพิ่มประสิทธิภาพ view สำหรับประสิทธิภาพแบตเตอรี่ - ✅ ทดสอบในขนาด widget ที่รองรับทั้งหมด --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/ios/widgetkit-ios17-interactive-widgets-app-intents