WidgetKit iOS 17-26: App Intents ile Etkileşimli Widget'lar

WidgetKit ve App Intents ile etkileşimli iOS widget'ları oluşturmak için kapsamlı kılavuz. Düğmeler, anahtarlar, animasyonlar, iOS 26 Liquid Glass materyali ve WWDC 2026 güncellemelerini kapsar.

Modern iOS uygulamaları için etkileşimli widget'lar ve App Intents ile WidgetKit iOS 17+

iOS 17, WidgetKit'e ana ekrandan doğrudan kullanıcı eylemlerine yanıt veren native etkileşim getirdi. iOS 26, Liquid Glass materyali ve akcentli render modları ile bu deneyimi geliştirdi. Bu kılavuz, iOS 17'den iOS 26'ya kadar etkileşimli widget geliştirmesini kapsar.

Bu makalenin kapsamı

iOS 17-26 etkileşimli widget'ları oluşturma, proje kurulumundan animasyonlar, durum yönetimi ve en son WWDC 2026 App Intents özellikleri ile gelişmiş kalıplara kadar.

iOS 17+ Etkileşimli Widget Mimarisi

iOS 17+ widget etkileşimi App Intents framework'ü aracılığıyla çalışır. Uygulamayı açacak geleneksel deep link'lerin aksine, App Intents kodun doğrudan widget'tan yürütülmesine izin verir, ardından görüntüyü otomatik olarak yeni verilerle yeniler.

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 yapılandırmasını bildirir ve verileri sağlayacak provider'ı belirtir. .containerBackground özelliği, etkileşimli widget'lar için iOS 17'den beri zorunludur ve iOS 26 uygun olduğunda Liquid Glass stilini otomatik olarak uygular.

Timeline Provider Oluşturma

Timeline Provider, widget'ın ne zaman ve nasıl yenileneceğini belirler. Etkileşimli widget'lar için App Intents tarafından tetiklenen değişikliklere de tepki vermelidir.

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 { $0 }
        )
        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, verilere erişmek için paylaşılan bir TaskDataManager kullanır. Bu yaklaşım, ana uygulama ve widget arasında senkronizasyonu garanti eder. İlgili durum yönetimi kalıpları için SwiftUI @Observable vs @State makalesine bakılabilir.

App Group zorunlu

Uygulama ve widget arasında veri paylaşmak için, proje capabilities'inde bir App Group yapılandırmak gerekir. UserDefaults veya dosyalar bu paylaşılan grubu kullanmalıdır.

Paylaşılan Veri Yöneticisi

Uygulama ve widget arasında veri paylaşımı, App Group aracılığıyla erişilebilen ortak bir konteyner gerektirir.

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: { $0.id == task.id }) {
            tasks[index] = task
            saveTasks(tasks)
        }
    }

    // Toggle completed state
    func toggleTaskCompletion(taskID: UUID) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { $0.id == taskID }) {
            tasks[index].isCompleted.toggle()
            saveTasks(tasks)
        }
    }
}

Bu yönetici tüm kalıcılık mantığını kapsar ve hem uygulama hem de widget App Intents tarafından kullanılacaktır.

Etkileşim için App Intent Oluşturma

App Intent, kullanıcı widget ile etkileşime girdiğinde yürütülen eylemi tanımlar. iOS bu eylemi arka planda yürütür ve ardından widget'ı otomatik olarak yeniler.

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

WidgetCenter.shared.reloadTimelines çağrısı eylemden sonra widget'ı anında yeniler ve anlık görsel geri bildirim sağlar.

iOS mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Etkileşimli Düğmelerle Widget Görünümü

Widget görünümü, eylem olarak intent ile standart SwiftUI Button bileşenini kullanır. iOS 17+, App Intent'i yürütmek için bu etkileşimleri otomatik olarak yakalar.

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 { !$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 {
        // 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()
        }
    }
}

Button(intent:) sözdizimi düğmeyi doğrudan App Intent'e bağlar. Dokunulduğunda iOS perform() yürütür ve ardından widget'ı otomatik olarak yeniler.

Widget'lar için Etkileşimli Toggle

Açık/kapalı türü eylemler için, Toggle bileşeni native iOS tarzıyla düğmeye alternatif sunar.

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: { $0.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, ikili durumlar için daha sezgisel bir etkileşim sağlar ve iOS tasarımlarına doğal olarak entegre olur.

Etkileşimli widget sınırlamaları

Widget'lar uyarılar, sheet veya gezinti gösteremez. Tüm eylemler bağımsız olmalı ve görünür durumu doğrudan güncellemelidir.

Yenileme Animasyonları ve Geçişler

iOS 17+, eylemden sonra widget yenileme sırasında geçişlerin animasyonuna izin verir. .contentTransition modifier'ı bu animasyonları kontrol eder.

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

.symbolEffect(.replace) ve .numericText() animasyonları durumlar arasında akıcı geçişler oluşturur ve kullanıcı deneyimini önemli ölçüde iyileştirir.

iOS 26 Liquid Glass ve Akcentli Render

iOS 26, widget'lar için Liquid Glass materyali ve akcentli render modları getirdi. Bu görsel güncellemeler .containerBackground kullanıldığında otomatik olarak uygulanır, ancak geliştiriciler render davranışını kontrol edebilir.

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

.widgetAccentedRenderingMode(.fullColor) modifier'ı aksentli ortamlarda görüntü renklerini korur. visionOS 26'dan gelen .systemExtraLarge ailesi artık iOS ve iPadOS'ta da kullanılabilir.

WWDC 2026 App Intents: Performans için EntityCollection

WWDC 2026, çok sayıda varlığı verimli bir şekilde işlemek için EntityCollection özelliğini tanıttı. Intent yürütülmeden önce tüm varlıkları çözümlemek yerine, yalnızca tanımlayıcılar iletilir.

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: $0) })
    }
    
    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: { $0.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: $0) }
    }
}

Bu kalıp toplu işlemler için kritiktir. Apple'ın dahili testleri, EntityCollection ile 1000 fotoğrafı etiketlemenin yavaştan "neredeyse anlık" hale geldiğini gösterdi.

İlerleme Gösterimli Uzun Süreli Intent'ler

WWDC 2026 ayrıca standart 30 saniyelik limiti aşan işlemler için, ilerlemenin Live Activity olarak görüntülendiği LongRunningIntent özelliğini tanıttı.

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

İlerleme Live Activity olarak görünür ve CancellableIntent iptal nedenini sağlayarak uygun temizlik yapılmasına olanak tanır. Swift eşzamanlılık kalıpları hakkında daha fazla bilgi için Swift Structured Concurrency makalesine bakılabilir.

AppIntentConfiguration ile Yapılandırılabilir Widget

Kullanıcı tarafından özelleştirilebilir widget'lar için (filtreler, kategoriler), AppIntentConfiguration StaticConfiguration'ın yerini alır.

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 { $0.priority == priority }
        }

        // Filter completed if needed
        if !config.showCompleted {
            tasks = tasks.filter { !$0.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])
    }
}

Kullanıcı artık uzun basma ile widget'ı yapılandırabilir ve uygulamada ek kod olmadan kişiselleştirilmiş bir deneyim elde edebilir. App Intents ve Siri entegrasyonu hakkında daha fazla bilgi için App Intents 2.0 ve Siri Shortcuts makalesine bakılabilir.

En İyi Uygulamalar ve Optimizasyonlar

Birkaç kalıp performanslı ve güvenilir etkileşimli widget'lar sağlar.

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 { !$0.isCompleted }.count
        self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.isCompleted }.count
    }
}

Bu optimizasyonlar bataryayı aşırı tüketmeyen tepkili widget'lar sağlar.

Widget'ları debug etme

Debug için Xcode'daki widget şemasını kullanın. Canvas önizleme cihaza yükleme olmadan farklı boyutları ve durumları test etmenizi sağlar.

Kaynaklar

Etkileşimli iOS Widget Geliştirme Kontrol Listesi

App Intents ile WidgetKit, widget'ları iOS uygulamalarının gerçek etkileşimli uzantılarına dönüştürür. iOS 17'de tanıtılan mimari, iOS 26'ya kadar stabil kalır ve görsel iyileştirmeler ile performans özellikleri eklenir.

Uygulama kontrol listesi

  • Uygulama ve widget extension arasında veri paylaşımı için App Group yapılandırın
  • Uygun yenileme politikası ile Timeline Provider oluşturun
  • Her kullanıcı eylemi için App Intents uygulayın
  • Etkileşim için Button(intent:) veya Toggle(intent:) kullanın
  • Veri değişikliğinden sonra WidgetCenter.shared.reloadTimelines çağırın
  • iOS 17+ uyumluluğu için zorunlu .containerBackground ekleyin
  • iOS 26 Liquid Glass desteği için .widgetAccentedRenderingMode kullanın
  • .contentTransition ile akıcı geçiş animasyonları uygulayın
  • Görsel geri bildirim için yükleme ve hata durumlarını yönetin
  • Çözümleme yükünü önlemek için toplu işlemlerde EntityCollection kullanın
  • .systemExtraLarge dahil tüm desteklenen widget boyutlarında test edin

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Günün meydan okuması

iOS kodundaki hatayı bulabilir misin?

Gerçek bir kod parçası, gizli bir hata, günde bir deneme. Denemek için hesap gerekmez.

Anthony Fillion-Maillet

Yazan:

Anthony Fillion-Maillet

SharpSkill kurucusu

10 yılı aşkın süredir fullstack geliştirici. SharpSkill’i yönetiyor ve burada yayımlanan her şeyden sorumlu.

24 Ağustos 2026 tarihinde güncellendi

Etiketler

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

Paylaş

İlgili makaleler