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

คู่มือฉบับสมบูรณ์ในการสร้าง iOS widget แบบโต้ตอบด้วย WidgetKit และ App Intents ปุ่ม สวิตช์ แอนิเมชัน และแนวปฏิบัติที่ดีที่สุดสำหรับ iOS 17+ ในปี 2026

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

iOS 17 ได้ปฏิวัติ WidgetKit โดยนำเสนอการโต้ตอบแบบ native วิดเจ็ตไม่ได้เป็นเพียงการแสดงผลแบบคงที่อีกต่อไป: ตอนนี้สามารถตอบสนองต่อการกระทำของผู้ใช้ได้โดยตรงจากหน้าจอหลัก โดยไม่ต้องเปิดแอปพลิเคชัน วิวัฒนาการครั้งสำคัญนี้พึ่งพาเฟรมเวิร์ก App Intents และมอบประสบการณ์การใช้งานที่ลื่นไหลและทันสมัย

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

บทความนี้นำเสนอการสร้างวิดเจ็ตแบบโต้ตอบ iOS 17+ ที่สมบูรณ์ ตั้งแต่การกำหนดค่าโปรเจกต์ไปจนถึงรูปแบบขั้นสูงพร้อมแอนิเมชันและการจัดการสถานะ

สถาปัตยกรรมของ Widget แบบโต้ตอบ

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

InteractiveWidgetArchitecture.swiftswift
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 ด้วย

TaskTimelineProvider.swiftswift
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<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)
    }
}

Provider ใช้ TaskDataManager ที่ใช้ร่วมกันเพื่อเข้าถึงข้อมูล วิธีการนี้รับประกันการซิงโครไนซ์ระหว่างแอปพลิเคชันหลักและ widget

App Group จำเป็น

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

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

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

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

    // 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 ทันทีหลังจากการกระทำ รับประกันการตอบสนองทางสายตาในทันที

พร้อมที่จะพิชิตการสัมภาษณ์ 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

    // ปรับให้เข้ากับขนาดของ 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

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

ข้อจำกัดของ 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) {
                // ไอคอนพร้อมแอนิเมชันการเปลี่ยนผ่าน
                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

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: "ต่ำ"
    ]
}

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

// 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 ที่ดีต้องการการจัดการกรณีข้อผิดพลาดและสถานะระดับกลางระหว่างการโต้ตอบ

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

        // จำลองการดำเนินการแบบ 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 แบบโต้ตอบจะมีประสิทธิภาพและเชื่อถือได้

WidgetBestPractices.swiftswift
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 ที่รองรับทั้งหมด

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

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

แท็ก

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

แชร์

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