WidgetKit iOS 17-26: Widget Interaktif dengan App Intents

Panduan lengkap untuk membuat widget iOS interaktif dengan WidgetKit dan App Intents. Tombol, toggle, animasi, material Liquid Glass iOS 26, dan pembaruan WWDC 2026.

WidgetKit iOS 17+ dengan widget interaktif dan App Intents untuk aplikasi iOS modern

iOS 17 memperkenalkan interaktivitas native ke WidgetKit, memungkinkan widget merespons tindakan pengguna langsung dari layar utama tanpa membuka aplikasi. iOS 26 menyempurnakan ini dengan material Liquid Glass dan mode rendering beraksen. Panduan ini mencakup pengembangan widget interaktif dari iOS 17 hingga iOS 26.

Yang dibahas artikel ini

Membuat widget interaktif iOS 17-26, mulai dari konfigurasi proyek hingga pola lanjutan dengan animasi, manajemen state, dan fitur App Intents WWDC 2026 terbaru.

Arsitektur Widget Interaktif di iOS 17+

Interaktivitas widget iOS 17+ bekerja melalui framework App Intents. Berbeda dengan deep link tradisional yang membuka aplikasi, App Intents memungkinkan eksekusi kode langsung dari widget, kemudian secara otomatis memperbarui tampilan dengan data baru.

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 mendeklarasikan konfigurasinya dan menentukan provider yang menyediakan data. Atribut .containerBackground wajib sejak iOS 17 untuk widget interaktif, dan iOS 26 secara otomatis menerapkan styling Liquid Glass bila sesuai.

Pembuatan Timeline Provider

Timeline Provider menentukan kapan dan bagaimana widget diperbarui. Untuk widget interaktif, ia juga harus merespons perubahan yang dipicu oleh 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 menggunakan TaskDataManager bersama untuk mengakses data. Pendekatan ini menjamin sinkronisasi antara aplikasi utama dan widget. Untuk pola manajemen state terkait, lihat SwiftUI @Observable vs @State.

App Group wajib

Untuk berbagi data antara aplikasi dan widget, perlu mengonfigurasi App Group di capabilities proyek. UserDefaults atau file harus menggunakan grup bersama ini.

Manajer Data Bersama

Berbagi data antara aplikasi dan widget memerlukan kontainer bersama yang dapat diakses melalui 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)
        }
    }
}

Manajer ini mengenkapsulasi semua logika persistensi dan akan digunakan baik oleh aplikasi maupun App Intents widget.

Pembuatan App Intent untuk Interaktivitas

App Intent mendefinisikan tindakan yang dijalankan ketika pengguna berinteraksi dengan widget. iOS menjalankan tindakan ini di latar belakang kemudian secara otomatis memperbarui 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()
    }
}

Panggilan ke WidgetCenter.shared.reloadTimelines memicu refresh segera widget setelah tindakan, memastikan feedback visual instan.

Siap menguasai wawancara iOS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

View Widget dengan Tombol Interaktif

View widget menggunakan komponen Button standar SwiftUI dengan intent sebagai tindakan. iOS 17+ secara otomatis menangkap interaksi ini untuk menjalankan 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()
        }
    }
}

Sintaks Button(intent:) secara langsung menghubungkan tombol ke App Intent. Saat disentuh, iOS menjalankan perform() kemudian secara otomatis memperbarui widget.

Toggle Interaktif untuk Widget

Untuk tindakan tipe on/off, komponen Toggle menawarkan alternatif tombol dengan gaya 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 menyediakan interaksi yang lebih intuitif untuk state biner dan terintegrasi secara alami dalam desain iOS.

Batasan widget interaktif

Widget tidak dapat menampilkan alert, sheet, atau navigasi. Semua tindakan harus mandiri dan memperbarui state yang terlihat secara langsung.

Animasi Refresh dan Transisi

iOS 17+ memungkinkan animasi transisi selama refresh widget setelah tindakan. Modifier .contentTransition mengontrol animasi ini.

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

Animasi .symbolEffect(.replace) dan .numericText() menciptakan transisi halus antar state, secara signifikan meningkatkan pengalaman pengguna.

iOS 26 Liquid Glass dan Rendering Beraksen

iOS 26 memperkenalkan material Liquid Glass dan mode rendering beraksen untuk widget. Pembaruan visual ini diterapkan secara otomatis saat menggunakan .containerBackground, tetapi pengembang dapat mengontrol perilaku 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) mempertahankan warna gambar di lingkungan beraksen. Family .systemExtraLarge, yang awalnya dari visionOS 26, sekarang tersedia di iOS dan iPadOS.

WWDC 2026 App Intents: EntityCollection untuk Performa

WWDC 2026 memperkenalkan EntityCollection untuk menangani sejumlah besar entitas secara efisien. Alih-alih menyelesaikan semua entitas sebelum eksekusi intent, hanya identifier yang diteruskan.

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

Pola ini sangat penting untuk operasi massal. Benchmark internal Apple menunjukkan pemberian tag 1000 foto berubah dari lambat menjadi "hampir instan" dengan EntityCollection.

Long-Running Intents dengan Tampilan Progress

WWDC 2026 juga memperkenalkan LongRunningIntent untuk operasi yang melebihi batas 30 detik standar, dengan progress ditampilkan sebagai 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 muncul sebagai Live Activity, dan CancellableIntent menyediakan alasan pembatalan sehingga pembersihan dapat ditangani dengan tepat. Untuk lebih lanjut tentang pola concurrency Swift, lihat Swift Structured Concurrency.

Widget yang Dapat Dikonfigurasi dengan AppIntentConfiguration

Untuk widget yang dapat disesuaikan pengguna (filter, kategori), AppIntentConfiguration menggantikan 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])
    }
}

Pengguna dapat mengonfigurasi widget melalui tekan lama, menyediakan pengalaman yang dipersonalisasi tanpa kode tambahan dalam aplikasi. Untuk lebih lanjut tentang App Intents dan integrasi Siri, lihat App Intents 2.0 dan Siri Shortcuts.

Praktik Terbaik dan Optimalisasi

Beberapa pola memastikan widget interaktif yang berkinerja tinggi dan andal.

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

Optimalisasi ini memastikan widget responsif yang tidak menghabiskan baterai secara berlebihan.

Debugging widget

Gunakan skema widget di Xcode untuk debugging. Pratinjau canvas memungkinkan pengujian berbagai ukuran dan state tanpa instalasi pada perangkat.

Sources

Checklist Pengembangan Widget iOS Interaktif

WidgetKit dengan App Intents mengubah widget menjadi ekstensi interaktif sejati dari aplikasi iOS. Arsitektur yang diperkenalkan di iOS 17 tetap stabil hingga iOS 26, dengan penyempurnaan visual dan fitur performa ditambahkan sepanjang jalan.

Checklist implementasi

  • Konfigurasikan App Group untuk berbagi data antara aplikasi dan ekstensi widget
  • Buat Timeline Provider dengan kebijakan refresh yang sesuai
  • Implementasikan App Intents untuk setiap tindakan pengguna
  • Gunakan Button(intent:) atau Toggle(intent:) untuk interaktivitas
  • Panggil WidgetCenter.shared.reloadTimelines setelah modifikasi data
  • Tambahkan .containerBackground wajib untuk kompatibilitas iOS 17+
  • Gunakan .widgetAccentedRenderingMode untuk dukungan Liquid Glass iOS 26
  • Implementasikan animasi transisi halus dengan .contentTransition
  • Tangani state pemuatan dan error untuk feedback visual
  • Gunakan EntityCollection untuk operasi massal untuk menghindari overhead resolusi
  • Uji pada semua ukuran widget yang didukung termasuk .systemExtraLarge

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tantangan harian

Bisakah kamu menemukan bug di iOS?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 24 Agustus 2026

Tag

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

Bagikan

Artikel terkait