WidgetKit iOS 17+: 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 và thực hành tốt nhất cho iOS 17+ vào năm 2026.

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

iOS 17 đã cách mạng hóa WidgetKit bằng cách giới thiệu khả năng tương tác native. Widget không còn là hiển thị tĩnh nữa: giờ đây chúng có thể phản hồi các hành động của người dùng trực tiếp từ màn hình chính, mà không cần mở ứng dụng. Sự tiến hóa quan trọng này dựa trên framework App Intents và mang lại trải nghiệm người dùng mượt mà, hiện đại.

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

Bài viết này trình bày việc tạo đầy đủ widget tương tác iOS 17+, từ cấu hình dự án đến các mẫu nâng cao với hoạt ảnh và quản lý trạng thái.

Kiến Trúc Widget Tương Tác

Khả năng tương tác của 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

// Kiến trúc dựa trên ba thành phần chính:
// 1. Widget Timeline Provider - cung cấp dữ liệu
// 2. Widget View - hiển thị giao diện với Button/Toggle
// 3. App Intent - thực thi hành động khi chạm

struct TaskWidget: Widget {
    // Mã định danh duy nhất của widget
    let kind: String = "TaskWidget"

    var body: some WidgetConfiguration {
        // StaticConfiguration cho widget không có tham số
        StaticConfiguration(
            kind: kind,
            provider: TaskTimelineProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
                // Bắt buộc cho App Intents
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Nhiệm vụ")
        .description("Quản lý nhiệm vụ của bạn từ màn hình chính.")
        .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.

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 gây ra bởi App Intents.

TaskTimelineProvider.swiftswift
import WidgetKit
import SwiftUI

// Entry đại diện cho trạng thái widget tại một thời điểm
struct TaskEntry: TimelineEntry {
    let date: Date
    let tasks: [Task]

    // Trạng thái tải cho phản hồi trực quan
    var isLoading: Bool = false
}

// Mô hình dữ liệu được chia sẻ giữa ứng dụng và 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 hiển thị khi tải ban đầu
    func placeholder(in context: Context) -> TaskEntry {
        TaskEntry(
            date: Date(),
            tasks: [
                Task(id: UUID(), title: "Nhiệm vụ mẫu", isCompleted: false, priority: .medium)
            ]
        )
    }

    // Snapshot cho thư viện 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 đầy đủ với chính sách làm mới
    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)))

        // Làm mới sau 15 phút hoặc sau hành động người dùng
        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.

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 để truy cập toàn cục
    static let shared = TaskDataManager()

    // Mã định danh App Group được cấu hình trong Xcode
    private let appGroupID = "group.com.example.taskapp"

    // UserDefaults được chia sẻ giữa ứng dụng và widget
    private var sharedDefaults: UserDefaults? {
        UserDefaults(suiteName: appGroupID)
    }

    private let tasksKey = "tasks"

    private init() {}

    // Lấy nhiệm vụ từ bộ nhớ chia sẻ
    func fetchTasks() -> [Task] {
        guard let data = sharedDefaults?.data(forKey: tasksKey),
              let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
            return []
        }
        return tasks
    }

    // Lưu với thông báo widget
    func saveTasks(_ tasks: [Task]) {
        guard let data = try? JSONEncoder().encode(tasks) else { return }
        sharedDefaults?.set(data, forKey: tasksKey)
    }

    // Cập nhật một nhiệm vụ cụ thể
    func updateTask(_ task: Task) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks[index] = task
            saveTasks(tasks)
        }
    }

    // Chuyển đổi trạng thái hoàn thành
    func toggleTaskCompletion(taskID: UUID) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { $0.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 để chuyển đổi trạng thái nhiệm vụ
struct ToggleTaskIntent: AppIntent {
    // Tiêu đề hiển thị trong phím tắt Siri
    static var title: LocalizedStringResource = "Chuyển đổi trạng thái nhiệm vụ"

    // Mô tả cho khả năng tiếp cận
    static var description = IntentDescription("Đánh dấu nhiệm vụ là hoàn thành hoặc chưa hoàn thành.")

    // Tham số: ID nhiệm vụ cần thay đổi
    @Parameter(title: "ID nhiệm vụ")
    var taskID: String

    // Initializer bắt buộc cho AppIntent
    init() {}

    // Initializer với tham số để tạo từ view
    init(taskID: UUID) {
        self.taskID = taskID.uuidString
    }

    // Thực thi hành động
    func perform() async throws -> some IntentResult {
        // Chuyển đổi ID chuỗi sang UUID
        guard let uuid = UUID(uuidString: taskID) else {
            return .result()
        }

        // Cập nhật nhiệm vụ
        TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)

        // Yêu cầu làm mới widget
        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

    // Thích ứng với kích thước widget
    @Environment(\.widgetFamily) var family

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // Header với tiêu đề và bộ đếm
            headerView

            // Danh sách nhiệm vụ với nút tương tác
            ForEach(entry.tasks.prefix(tasksLimit)) { task in
                TaskRowView(task: task)
            }

            Spacer(minLength: 0)
        }
        .padding()
    }

    // Số lượng nhiệm vụ theo kích thước
    private var tasksLimit: Int {
        switch family {
        case .systemSmall: return 2
        case .systemMedium: return 3
        default: return 4
        }
    }

    private var headerView: some View {
        HStack {
            Text("Nhiệm vụ")
                .font(.headline)
                .fontWeight(.bold)

            Spacer()

            // Huy hiệu với số nhiệm vụ còn lại
            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 {
        // Nút với App Intent là hành động
        Button(intent: ToggleTaskIntent(taskID: task.id)) {
            HStack(spacing: 12) {
                // Chỉ báo hoàn thành
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title3)
                    .foregroundStyle(task.isCompleted ? .green : .secondary)

                // Tiêu đề nhiệm vụ
                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
                    .lineLimit(1)

                Spacer()

                // Chỉ báo ưu tiên
                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

// Intent cụ thể cho Toggle với trạng thái rõ ràng
struct SetTaskCompletionIntent: AppIntent {
    static var title: LocalizedStringResource = "Đặt trạng thái nhiệm vụ"

    @Parameter(title: "ID nhiệm vụ")
    var taskID: String

    // Trạng thái mục tiêu: true = hoàn thành, false = chưa hoàn thành
    @Parameter(title: "Hoàn thành")
    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 }) {
            // Đặt trạng thái rõ ràng (không phải 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 tương tác với 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) {
                // Biểu tượng với hoạt ảnh chuyển tiếp
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title3)
                    .foregroundStyle(task.isCompleted ? .green : .secondary)
                    // Hoạt ảnh biểu tượng khi thay đổi
                    .contentTransition(.symbolEffect(.replace))

                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
                    // Hoạt ảnh văn bản
                    .contentTransition(.opacity)

                Spacer()
            }
            .padding(.vertical, 6)
            .padding(.horizontal, 10)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear)
            )
            // Hoạt ảnh nền
            .animation(.easeInOut(duration: 0.3), value: task.isCompleted)
        }
        .buttonStyle(.plain)
    }
}

// Widget với vô hiệu hóa hoạt ảnh
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("Nhiệm vụ Có Hoạt Ảnh")
        .description("Widget với hoạt ảnh mượt mà.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
        // Kích hoạt hoạt ảnh nội dung
        .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("Nhiệm vụ")
                .font(.headline.bold())

            Spacer()

            let completed = entry.tasks.filter(\.isCompleted).count
            let total = entry.tasks.count

            // Tiến trình có hoạt ảnh
            Text("\(completed)/\(total)")
                .font(.caption.bold())
                .foregroundStyle(.secondary)
                .contentTransition(.numericText())
        }
    }
}

Hoạt ảnh .symbolEffect(.replace).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.

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

// Cấu hình được hiển thị cho người dùng
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Cấu hình nhiệm vụ"
    static var description = IntentDescription("Tùy chỉnh hiển thị nhiệm vụ.")

    // Bộ lọc theo ưu tiên
    @Parameter(title: "Ưu tiên", default: .all)
    var priorityFilter: PriorityFilter

    // Hiển thị nhiệm vụ đã hoàn thành
    @Parameter(title: "Hiển thị đã hoàn thành", default: true)
    var showCompleted: Bool

    // Số lượng nhiệm vụ tối đa
    @Parameter(title: "Số nhiệm vụ", default: 3)
    var maxTasks: Int
}

// Enum cho bộ lọc ưu tiên
enum PriorityFilter: String, AppEnum {
    case all
    case high
    case medium
    case low

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Ưu tiên"

    static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
        .all: "Tất cả",
        .high: "Cao",
        .medium: "Trung bình",
        .low: "Thấp"
    ]
}

// Provider thích ứng với cấu hình
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))
    }

    // Áp dụng bộ lọc cấu hình
    private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
        var tasks = TaskDataManager.shared.fetchTasks()

        // Lọc theo ưu tiên
        if config.priorityFilter != .all {
            let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
            tasks = tasks.filter { $0.priority == priority }
        }

        // Lọc đã hoàn thành nếu cần
        if !config.showCompleted {
            tasks = tasks.filter { !$0.isCompleted }
        }

        // Giới hạn số lượng
        return Array(tasks.prefix(config.maxTasks))
    }
}

// Widget với cấu hình người dùng
struct ConfigurableTaskWidget: Widget {
    let kind: String = "ConfigurableTaskWidget"

    var body: some WidgetConfiguration {
        // AppIntentConfiguration cho widget có thể cấu hình
        AppIntentConfiguration(
            kind: kind,
            intent: TaskWidgetConfigurationIntent.self,
            provider: ConfigurableTaskProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Nhiệm vụ Tùy Chỉnh")
        .description("Lọc và tùy chỉnh nhiệm vụ của bạn.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

Người dùng giờ đây 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.

Xử Lý Lỗi và Trạng Thái Tải

UX tốt yêu cầu xử lý các trường hợp lỗi và trạng thái trung gian trong quá trình tương tác.

TaskIntentWithFeedback.swiftswift
import AppIntents
import WidgetKit

struct ToggleTaskWithFeedbackIntent: AppIntent {
    static var title: LocalizedStringResource = "Chuyển đổi nhiệm vụ với phản hồi"

    @Parameter(title: "ID nhiệm vụ")
    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 {
            // Trả về thất bại im lặng
            return .result(value: false)
        }

        // Mô phỏng hoạt động async (ví dụ: đồng bộ máy chủ)
        do {
            try await Task.sleep(for: .milliseconds(100))

            TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
            WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")

            return .result(value: true)
        } catch {
            // Lỗi: không cập nhật widget
            return .result(value: false)
        }
    }
}

// View với trạng thái tải
struct TaskRowWithLoadingView: View {
    let task: Task
    @State private var isLoading = false

    var body: some View {
        Button(intent: ToggleTaskIntent(taskID: task.id)) {
            HStack(spacing: 12) {
                // Chỉ báo có điều kiện
                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)
    }
}

Phản hồi trực quan tức thì (độ mờ giảm, chỉ báo tải) thông báo cho người dùng rằng hành động của họ đã được ghi lại.

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. Luôn vô hiệu hóa cache sau khi sửa đổi
final class WidgetRefreshManager {
    static func refreshAllWidgets() {
        // Làm mới tất cả widget của ứng dụng
        WidgetCenter.shared.reloadAllTimelines()
    }

    static func refreshWidget(kind: String) {
        // Làm mới một widget cụ thể
        WidgetCenter.shared.reloadTimelines(ofKind: kind)
    }

    // Gọi từ ứng dụng sau khi sửa đổi dữ liệu
    static func notifyDataChanged() {
        Task { @MainActor in
            refreshAllWidgets()
        }
    }
}

// 2. Giới hạn độ phức tạp của view
struct OptimizedWidgetView: View {
    let entry: TaskEntry

    var body: some View {
        // Ưu tiên view đơn giản không có GeometryReader
        VStack(alignment: .leading, spacing: 8) {
            ForEach(entry.tasks.prefix(3)) { task in
                // Thành phần nhẹ
                minimalTaskRow(task)
            }
        }
        .padding()
    }

    // View tối thiểu để tăng hiệu suất
    @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. Sử dụng @AppStorage cho trạng thái đơn giản
struct QuickSettingsWidgetView: View {
    // Truy cập trực tiếp vào UserDefaults chia sẻ
    @AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
    private var showCompleted = true

    var body: some View {
        // Trạng thái duy trì giữa các lần làm mới
        Text(showCompleted ? "Hiển thị tất cả" : "Ẩn đã hoàn thành")
    }
}

// 4. Tính toán trước dữ liệu trong provider
struct OptimizedTaskEntry: TimelineEntry {
    let date: Date
    let tasks: [Task]

    // Dữ liệu được tính toán trước
    let completedCount: Int
    let pendingCount: Int
    let highPriorityCount: Int

    init(date: Date, tasks: [Task]) {
        self.date = date
        self.tasks = tasks

        // Các tính toán được thực hiện một lần
        self.completedCount = tasks.filter(\.isCompleted).count
        self.pendingCount = tasks.filter { !$0.isCompleted }.count
        self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.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ị.

Kết Luận

WidgetKit iOS 17+ 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 khai báo này đơn giản hóa đáng kể việc phát triển trong khi cung cấp trải nghiệm người dùng native và mượt mà.

Danh Sách Kiểm Tra Widget Tương Tác iOS 17+

  • ✅ Cấu hình App Group để chia sẻ dữ liệu
  • ✅ Tạo Timeline Provider với làm mới phù hợp
  • ✅ Triển khai App Intents cho mỗi hành độ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
  • ✅ Thêm .containerBackground bắt buộc cho iOS 17+
  • ✅ Triển khai hoạt ảnh chuyển tiếp mượt mà
  • ✅ Xử lý trạng thái tải và lỗi
  • ✅ Tối ưu hóa view cho hiệu suất pin
  • ✅ Kiểm tra trên tất cả các kích thước widget được hỗ trợ

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ẻ

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

Chia sẻ

Bài viết liên quan