# 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. - Published: 2026-03-04 - Updated: 2026-04-29 - Author: SharpSkill - Tags: widgetkit, ios, app-intents, swift, widgets - Reading time: 14 min --- 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. ```swift // InteractiveWidgetArchitecture.swift 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. ```swift // TaskTimelineProvider.swift 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) -> 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. ```swift // TaskDataManager.swift 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. ```swift // ToggleTaskIntent.swift 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ì. ## 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. ```swift // TaskWidgetView.swift 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. ```swift // ToggleWidgetView.swift 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. ```swift // AnimatedTaskWidgetView.swift import SwiftUI import WidgetKit struct AnimatedTaskRowView: View { let task: Task var body: some View { Button(intent: ToggleTaskIntent(taskID: task.id)) { HStack(spacing: 12) { // 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)` 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. ## 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. ```swift // ConfigurableTaskWidget.swift 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 { 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. ```swift // TaskIntentWithFeedback.swift 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 { 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. ```swift // WidgetBestPractices.swift 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ợ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/ios/widgetkit-ios17-interactive-widgets-app-intents