WidgetKit iOS 17-26: Widget Tương Tác với App Intents

Hướng dẫn đầy đủ tạo widget iOS tương tác với WidgetKit và App Intents. Nút bấm, công tắc, hoạt ảnh, material Liquid Glass iOS 26, và cập nhật WWDC 2026.

WidgetKit iOS 17+ với widget tương tác và App Intents cho ứng dụng iOS hiện đại

iOS 17 đã giới thiệu khả năng tương tác native cho WidgetKit, cho phép widget phản hồi hành động người dùng trực tiếp từ màn hình chính mà không cần mở ứng dụng. iOS 26 cải tiến điều này với material Liquid Glass và các chế độ rendering có điểm nhấn. Hướng dẫn này bao gồm phát triển widget tương tác từ iOS 17 đến iOS 26.

Nội dung bài viết này

Tạo widget tương tác iOS 17-26, từ thiết lập dự án đến các mẫu nâng cao với hoạt ảnh, quản lý trạng thái, và các tính năng App Intents WWDC 2026 mới nhất.

Kiến Trúc Widget Tương Tác trong iOS 17+

Khả năng tương tác widget iOS 17+ hoạt động thông qua framework App Intents. Khác với deep link truyền thống mở ứng dụng, App Intents cho phép thực thi mã trực tiếp từ widget, sau đó tự động làm mới hiển thị với dữ liệu mới.

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 khai báo cấu hình của nó và chỉ định provider sẽ cung cấp dữ liệu. Thuộc tính .containerBackground là bắt buộc kể từ iOS 17 cho widget tương tác, và iOS 26 tự động áp dụng styling Liquid Glass khi thích hợp.

Tạo Timeline Provider

Timeline Provider quyết định khi nào và cách widget được làm mới. Đối với widget tương tác, nó cũng phải phản hồi các thay đổi được kích hoạt bởi 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 sử dụng TaskDataManager được chia sẻ để truy cập dữ liệu. Cách tiếp cận này đảm bảo đồng bộ hóa giữa ứng dụng chính và widget. Để biết các mẫu quản lý trạng thái liên quan, xem SwiftUI @Observable vs @State.

App Group bắt buộc

Để chia sẻ dữ liệu giữa ứng dụng và widget, cần cấu hình App Group trong capabilities của dự án. UserDefaults hoặc tệp phải sử dụng nhóm chia sẻ này.

Trình Quản Lý Dữ Liệu Chia Sẻ

Việc chia sẻ dữ liệu giữa ứng dụng và widget yêu cầu một container chung có thể truy cập qua 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)
        }
    }
}

Trình quản lý này đóng gói tất cả logic lưu trữ và sẽ được sử dụng bởi cả ứng dụng và App Intents của widget.

Tạo App Intent cho Tương Tác

App Intent định nghĩa hành động được thực thi khi người dùng tương tác với widget. iOS thực thi hành động này ở chế độ nền sau đó tự động làm mới 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()
    }
}

Lời gọi WidgetCenter.shared.reloadTimelines kích hoạt làm mới widget ngay lập tức sau hành động, đảm bảo phản hồi trực quan tức thì.

Sẵn sàng chinh phục phỏng vấn iOS?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

View Widget với Nút Tương Tác

View widget sử dụng thành phần Button chuẩn của SwiftUI với intent là hành động. iOS 17+ tự động chặn các tương tác này để thực thi 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()
        }
    }
}

Cú pháp Button(intent:) kết nối trực tiếp nút với App Intent. Khi chạm, iOS thực thi perform() rồi tự động làm mới widget.

Toggle Tương Tác cho Widget

Đối với hành động kiểu bật/tắt, thành phần Toggle cung cấp một thay thế cho nút với phong cách 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 cung cấp tương tác trực quan hơn cho trạng thái nhị phân và tích hợp tự nhiên trong thiết kế iOS.

Hạn chế của widget tương tác

Widget không thể hiển thị cảnh báo, sheet hoặc điều hướng. Tất cả các hành động phải tự chứa và cập nhật trực tiếp trạng thái có thể nhìn thấy.

Hoạt Ảnh Làm Mới và Chuyển Tiếp

iOS 17+ cho phép hoạt ảnh chuyển tiếp trong khi làm mới widget sau hành động. Modifier .contentTransition kiểm soát các hoạt ảnh này.

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

Hoạt ảnh .symbolEffect(.replace) và .numericText() tạo ra các chuyển tiếp mượt mà giữa các trạng thái, cải thiện đáng kể trải nghiệm người dùng.

iOS 26 Liquid Glass và Rendering Có Điểm Nhấn

iOS 26 giới thiệu material Liquid Glass và các chế độ rendering có điểm nhấn cho widget. Các cập nhật trực quan này được áp dụng tự động khi sử dụng .containerBackground, nhưng nhà phát triển có thể kiểm soát hành vi 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) giữ nguyên màu sắc hình ảnh trong môi trường có điểm nhấn. Family .systemExtraLarge, ban đầu từ visionOS 26, hiện có sẵn trên iOS và iPadOS.

WWDC 2026 App Intents: EntityCollection cho Hiệu Suất

WWDC 2026 giới thiệu EntityCollection để xử lý số lượng lớn entity một cách hiệu quả. Thay vì giải quyết tất cả entity trước khi thực thi intent, chỉ có identifier được truyền.

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

Mẫu này rất quan trọng cho các thao tác hàng loạt. Benchmark nội bộ của Apple cho thấy việc gắn thẻ 1000 ảnh chuyển từ chậm sang "gần như tức thì" với EntityCollection.

Long-Running Intents với Hiển Thị Progress

WWDC 2026 cũng giới thiệu LongRunningIntent cho các thao tác vượt quá giới hạn 30 giây tiêu chuẩn, với progress được hiển thị dưới dạng 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 xuất hiện dưới dạng Live Activity, và CancellableIntent cung cấp lý do hủy để việc dọn dẹp có thể được xử lý phù hợp. Để biết thêm về các mẫu concurrency Swift, xem Swift Structured Concurrency.

Widget Có Thể Cấu Hình với AppIntentConfiguration

Đối với widget có thể tùy chỉnh bởi người dùng (bộ lọc, danh mục), AppIntentConfiguration thay thế 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])
    }
}

Người dùng có thể cấu hình widget thông qua nhấn lâu, mang lại trải nghiệm cá nhân hóa mà không cần mã bổ sung trong ứng dụng. Để biết thêm về App Intents và tích hợp Siri, xem App Intents 2.0 và Siri Shortcuts.

Thực Hành Tốt Nhất và Tối Ưu Hóa

Một số mẫu đảm bảo widget tương tác hiệu suất cao và đáng tin cậy.

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

Những tối ưu này đảm bảo widget phản hồi nhanh không tiêu tốn quá nhiều pin.

Gỡ lỗi widget

Sử dụng scheme widget trong Xcode để gỡ lỗi. Xem trước canvas cho phép kiểm tra các kích thước và trạng thái khác nhau mà không cần cài đặt trên thiết bị.

Sources

Checklist Phát Triển Widget iOS Tương Tác

WidgetKit với App Intents biến widget thành các phần mở rộng tương tác thực sự của ứng dụng iOS. Kiến trúc được giới thiệu trong iOS 17 vẫn ổn định qua iOS 26, với các cải tiến trực quan và tính năng hiệu suất được thêm vào theo thời gian.

Checklist triển khai

  • Cấu hình App Group để chia sẻ dữ liệu giữa ứng dụng và widget extension
  • Tạo Timeline Provider với chính sách làm mới phù hợp
  • Triển khai App Intents cho mỗi hành động người dùng
  • Sử dụng Button(intent:) hoặc Toggle(intent:) cho tương tác
  • Gọi WidgetCenter.shared.reloadTimelines sau khi sửa đổi dữ liệu
  • Thêm .containerBackground bắt buộc cho tương thích iOS 17+
  • Sử dụng .widgetAccentedRenderingMode để hỗ trợ Liquid Glass iOS 26
  • Triển khai hoạt ảnh chuyển tiếp mượt mà với .contentTransition
  • Xử lý trạng thái tải và lỗi để phản hồi trực quan
  • Sử dụng EntityCollection cho các thao tác hàng loạt để tránh overhead giải quyết
  • Kiểm tra trên tất cả các kích thước widget được hỗ trợ bao gồm .systemExtraLarge

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thử thách hôm nay

Bạn có tìm ra lỗi trong iOS không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 24 tháng 8, 2026

Thẻ

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

Chia sẻ

Bài viết liên quan