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

iOS 17 ได้ปฏิวัติ WidgetKit โดยนำเสนอการโต้ตอบแบบ native วิดเจ็ตไม่ได้เป็นเพียงการแสดงผลแบบคงที่อีกต่อไป: ตอนนี้สามารถตอบสนองต่อการกระทำของผู้ใช้ได้โดยตรงจากหน้าจอหลัก โดยไม่ต้องเปิดแอปพลิเคชัน วิวัฒนาการครั้งสำคัญนี้พึ่งพาเฟรมเวิร์ก App Intents และมอบประสบการณ์การใช้งานที่ลื่นไหลและทันสมัย
บทความนี้นำเสนอการสร้างวิดเจ็ตแบบโต้ตอบ iOS 17+ ที่สมบูรณ์ ตั้งแต่การกำหนดค่าโปรเจกต์ไปจนถึงรูปแบบขั้นสูงพร้อมแอนิเมชันและการจัดการสถานะ
สถาปัตยกรรมของ Widget แบบโต้ตอบ
ความสามารถในการโต้ตอบของ widget iOS 17+ ทำงานผ่านเฟรมเวิร์ก App Intents ต่างจาก deep link แบบดั้งเดิมที่จะเปิดแอปพลิเคชัน App Intents อนุญาตให้รันโค้ดโดยตรงจาก widget จากนั้นรีเฟรชการแสดงผลโดยอัตโนมัติด้วยข้อมูลใหม่
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 ด้วย
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
ในการแชร์ข้อมูลระหว่างแอปและ widget ต้องกำหนดค่า App Group ใน capabilities ของโปรเจกต์ UserDefaults หรือไฟล์ต้องใช้กลุ่มที่ใช้ร่วมกันนี้
ตัวจัดการข้อมูลที่ใช้ร่วมกัน
การแชร์ข้อมูลระหว่างแอปพลิเคชันและ widget ต้องการคอนเทนเนอร์ทั่วไปที่เข้าถึงได้ผ่าน App Group
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 โดยอัตโนมัติ
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
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
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 ไม่สามารถแสดง alert, sheet หรือการนำทาง การกระทำทั้งหมดต้องเป็นแบบอิสระและอัปเดตสถานะที่มองเห็นได้โดยตรง
แอนิเมชันการรีเฟรชและการเปลี่ยนผ่าน
iOS 17+ อนุญาตให้สร้างแอนิเมชันการเปลี่ยนผ่านระหว่างการรีเฟรช widget หลังจากการกระทำ Modifier .contentTransition ควบคุมแอนิเมชันเหล่านี้
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
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 ที่ดีต้องการการจัดการกรณีข้อผิดพลาดและสถานะระดับกลางระหว่างการโต้ตอบ
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 แบบโต้ตอบจะมีประสิทธิภาพและเชื่อถือได้
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 ที่ตอบสนองได้ดีโดยไม่ใช้แบตเตอรี่มากเกินไป
ใช้ 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 ที่รองรับทั้งหมด
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

App Intents และ Siri Shortcuts: ระบบอัตโนมัติ iOS ขั้นสูงปี 2026
คู่มือฉบับสมบูรณ์เกี่ยวกับ App Intents และ Siri Shortcuts สำหรับ iOS 18+ สร้างคำสั่ง Siri ที่กำหนดเอง รวม Apple Intelligence และทำให้แอป Swift เป็นอัตโนมัติในปี 2026

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

คำถามสัมภาษณ์การเข้าถึง iOS ในปี 2026: VoiceOver และ Dynamic Type
เตรียมตัวสัมภาษณ์ iOS ด้วยคำถามสำคัญเรื่องการเข้าถึง: VoiceOver, Dynamic Type, traits เชิงความหมาย และการตรวจสอบ.