# WidgetKit iOS 17+: App Intents ile Etkileşimli Widget'lar > WidgetKit ve App Intents ile etkileşimli iOS widget'ları oluşturmak için kapsamlı kılavuz. iOS 17+ için 2026'da düğmeler, anahtarlar, animasyonlar ve en iyi uygulamalar. - Published: 2026-03-04 - Updated: 2026-04-29 - Author: SharpSkill - Tags: widgetkit, ios, app-intents, swift, widgets - Reading time: 14 min --- iOS 17, native etkileşim sunarak WidgetKit'te devrim yarattı. Widget'lar artık statik gösterimler değil: artık uygulamayı açmadan ana ekrandan doğrudan kullanıcı eylemlerine yanıt verebiliyorlar. Bu önemli evrim, App Intents framework'üne dayanıyor ve akıcı, modern bir kullanıcı deneyimi sunuyor. > **Bu makalenin kapsamı** > > Bu makale, proje yapılandırmasından animasyonlar ve durum yönetimi ile gelişmiş kalıplara kadar iOS 17+ etkileşimli widget'ların eksiksiz oluşturulmasını sunmaktadır. ## Etkileşimli Widget Mimarisi iOS 17+ widget'larının 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. ```swift // InteractiveWidgetArchitecture.swift import WidgetKit import SwiftUI import AppIntents // Mimari üç ana bileşene dayanır: // 1. Widget Timeline Provider - verileri sağlar // 2. Widget View - Button/Toggle ile arayüzü gösterir // 3. App Intent - dokunduğunda eylemi yürütür struct TaskWidget: Widget { // Benzersiz widget tanımlayıcısı let kind: String = "TaskWidget" var body: some WidgetConfiguration { // Parametresiz widget'lar için StaticConfiguration StaticConfiguration( kind: kind, provider: TaskTimelineProvider() ) { entry in TaskWidgetView(entry: entry) // App Intents için zorunlu .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Görevler") .description("Ana ekrandan görevlerinizi yönetin.") .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. ## 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. ```swift // TaskTimelineProvider.swift import WidgetKit import SwiftUI // Belirli bir andaki widget durumunu temsil eden Entry struct TaskEntry: TimelineEntry { let date: Date let tasks: [Task] // Görsel geri bildirim için yükleme durumu var isLoading: Bool = false } // Uygulama ve widget arasında paylaşılan veri modeli 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 { // İlk yükleme sırasında gösterilen placeholder func placeholder(in context: Context) -> TaskEntry { TaskEntry( date: Date(), tasks: [ Task(id: UUID(), title: "Örnek görev", isCompleted: false, priority: .medium) ] ) } // Widget galerisi için snapshot func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) { let entry = TaskEntry( date: Date(), tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { $0 } ) completion(entry) } // Yenileme politikası ile tam timeline func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let tasks = TaskDataManager.shared.fetchTasks() let entry = TaskEntry(date: Date(), tasks: Array(tasks.prefix(3))) // 15 dakika sonra veya kullanıcı eyleminden sonra yenileme 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. > **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. ```swift // TaskDataManager.swift import Foundation final class TaskDataManager { // Global erişim için Singleton static let shared = TaskDataManager() // Xcode'da yapılandırılmış App Group tanımlayıcısı private let appGroupID = "group.com.example.taskapp" // Uygulama ve widget arasında paylaşılan UserDefaults private var sharedDefaults: UserDefaults? { UserDefaults(suiteName: appGroupID) } private let tasksKey = "tasks" private init() {} // Görevleri paylaşılan depolamadan alır func fetchTasks() -> [Task] { guard let data = sharedDefaults?.data(forKey: tasksKey), let tasks = try? JSONDecoder().decode([Task].self, from: data) else { return [] } return tasks } // Widget bildirimi ile kaydeder func saveTasks(_ tasks: [Task]) { guard let data = try? JSONEncoder().encode(tasks) else { return } sharedDefaults?.set(data, forKey: tasksKey) } // Belirli bir görevi günceller func updateTask(_ task: Task) { var tasks = fetchTasks() if let index = tasks.firstIndex(where: { $0.id == task.id }) { tasks[index] = task saveTasks(tasks) } } // Tamamlanma durumunu değiştirir 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. ```swift // ToggleTaskIntent.swift import AppIntents import WidgetKit // Görev durumunu değiştirme intent'i struct ToggleTaskIntent: AppIntent { // Siri kısayollarında gösterilen başlık static var title: LocalizedStringResource = "Görev durumunu değiştir" // Erişilebilirlik için açıklama static var description = IntentDescription("Bir görevi tamamlandı veya tamamlanmadı olarak işaretler.") // Parametre: değiştirilecek görevin ID'si @Parameter(title: "Görev ID'si") var taskID: String // AppIntent için gerekli initializer init() {} // View'dan oluşturma için parametreli initializer init(taskID: UUID) { self.taskID = taskID.uuidString } // Eylem yürütme func perform() async throws -> some IntentResult { // String ID'nin UUID'ye dönüşümü guard let uuid = UUID(uuidString: taskID) else { return .result() } // Görevin güncellenmesi TaskDataManager.shared.toggleTaskCompletion(taskID: uuid) // Widget yenilemesi ister 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. ## 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. ```swift // TaskWidgetView.swift import SwiftUI import WidgetKit struct TaskWidgetView: View { let entry: TaskEntry // Widget boyutuna uyum @Environment(\.widgetFamily) var family var body: some View { VStack(alignment: .leading, spacing: 8) { // Başlık ve sayaç ile header headerView // Etkileşimli düğmelerle görev listesi ForEach(entry.tasks.prefix(tasksLimit)) { task in TaskRowView(task: task) } Spacer(minLength: 0) } .padding() } // Boyuta göre görev sayısı private var tasksLimit: Int { switch family { case .systemSmall: return 2 case .systemMedium: return 3 default: return 4 } } private var headerView: some View { HStack { Text("Görevler") .font(.headline) .fontWeight(.bold) Spacer() // Kalan görev sayısı ile rozet 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 { // Eylem olarak App Intent ile düğme Button(intent: ToggleTaskIntent(taskID: task.id)) { HStack(spacing: 12) { // Tamamlanma göstergesi Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundStyle(task.isCompleted ? .green : .secondary) // Görev başlığı Text(task.title) .font(.subheadline) .strikethrough(task.isCompleted) .foregroundStyle(task.isCompleted ? .secondary : .primary) .lineLimit(1) Spacer() // Öncelik göstergesi 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. ```swift // ToggleWidgetView.swift import SwiftUI import AppIntents // Açık durumlu Toggle için özel intent struct SetTaskCompletionIntent: AppIntent { static var title: LocalizedStringResource = "Görev durumunu ayarla" @Parameter(title: "Görev ID'si") var taskID: String // Hedef durum: true = tamamlandı, false = tamamlanmadı @Parameter(title: "Tamamlandı") 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 }) { // Durumu açıkça ayarlar (toggle değil) 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() // Intent ile etkileşimli toggle 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. ```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) { // Geçiş animasyonlu ikon Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundStyle(task.isCompleted ? .green : .secondary) // Değişimde ikon animasyonu .contentTransition(.symbolEffect(.replace)) Text(task.title) .font(.subheadline) .strikethrough(task.isCompleted) .foregroundStyle(task.isCompleted ? .secondary : .primary) // Metin animasyonu .contentTransition(.opacity) Spacer() } .padding(.vertical, 6) .padding(.horizontal, 10) .background( RoundedRectangle(cornerRadius: 8) .fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear) ) // Arka plan animasyonu .animation(.easeInOut(duration: 0.3), value: task.isCompleted) } .buttonStyle(.plain) } } // Animasyonlu invalidasyon ile widget 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("Animasyonlu Görevler") .description("Akıcı animasyonlu widget'lar.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) // İçerik animasyonlarının etkinleştirilmesi .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("Görevler") .font(.headline.bold()) Spacer() let completed = entry.tasks.filter(\.isCompleted).count let total = entry.tasks.count // Animasyonlu ilerleme 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. ## 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. ```swift // ConfigurableTaskWidget.swift import WidgetKit import SwiftUI import AppIntents // Kullanıcıya açık yapılandırma struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent { static var title: LocalizedStringResource = "Görev yapılandırması" static var description = IntentDescription("Görev görüntüsünü özelleştirin.") // Önceliğe göre filtre @Parameter(title: "Öncelik", default: .all) var priorityFilter: PriorityFilter // Tamamlanan görevleri göster @Parameter(title: "Tamamlananları göster", default: true) var showCompleted: Bool // Maksimum görev sayısı @Parameter(title: "Görev sayısı", default: 3) var maxTasks: Int } // Öncelik filtresi için enum enum PriorityFilter: String, AppEnum { case all case high case medium case low static var typeDisplayRepresentation: TypeDisplayRepresentation = "Öncelik" static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [ .all: "Tümü", .high: "Yüksek", .medium: "Orta", .low: "Düşük" ] } // Yapılandırmaya uyarlanmış provider 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)) } // Yapılandırma filtrelerini uygular private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] { var tasks = TaskDataManager.shared.fetchTasks() // Önceliğe göre filtreleme if config.priorityFilter != .all { let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium tasks = tasks.filter { $0.priority == priority } } // Gerekirse tamamlananları filtreleme if !config.showCompleted { tasks = tasks.filter { !$0.isCompleted } } // Sayı sınırlaması return Array(tasks.prefix(config.maxTasks)) } } // Kullanıcı yapılandırmalı widget struct ConfigurableTaskWidget: Widget { let kind: String = "ConfigurableTaskWidget" var body: some WidgetConfiguration { // Yapılandırılabilir widget'lar için AppIntentConfiguration AppIntentConfiguration( kind: kind, intent: TaskWidgetConfigurationIntent.self, provider: ConfigurableTaskProvider() ) { entry in TaskWidgetView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Özelleştirilmiş Görevler") .description("Görevlerinizi filtreleyin ve özelleştirin.") .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. ## Hata Yönetimi ve Yükleme Durumları İyi bir UX, etkileşimler sırasında hata durumlarını ve ara durumları yönetmeyi gerektirir. ```swift // TaskIntentWithFeedback.swift import AppIntents import WidgetKit struct ToggleTaskWithFeedbackIntent: AppIntent { static var title: LocalizedStringResource = "Görevi feedback ile değiştir" @Parameter(title: "Görev ID'si") 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 { // Sessiz hata döndürür return .result(value: false) } // Async işlem simülasyonu (örn. sunucu sync) do { try await Task.sleep(for: .milliseconds(100)) TaskDataManager.shared.toggleTaskCompletion(taskID: uuid) WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget") return .result(value: true) } catch { // Hata: widget'ı güncelleme return .result(value: false) } } } // Yükleme durumlu görünüm struct TaskRowWithLoadingView: View { let task: Task @State private var isLoading = false var body: some View { Button(intent: ToggleTaskIntent(taskID: task.id)) { HStack(spacing: 12) { // Koşullu gösterge 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) } } ``` Anında görsel geri bildirim (azaltılmış opaklık, yükleme göstergesi) kullanıcıya eyleminin kaydedildiğini bildirir. ## En İyi Uygulamalar ve Optimizasyonlar Birkaç kalıp performanslı ve güvenilir etkileşimli widget'lar sağlar. ```swift // WidgetBestPractices.swift import WidgetKit import SwiftUI // 1. Modifikasyondan sonra her zaman önbelleği geçersiz kıl final class WidgetRefreshManager { static func refreshAllWidgets() { // Tüm uygulama widget'larının yenilenmesi WidgetCenter.shared.reloadAllTimelines() } static func refreshWidget(kind: String) { // Belirli bir widget'ın yenilenmesi WidgetCenter.shared.reloadTimelines(ofKind: kind) } // Veri modifikasyonundan sonra uygulamadan çağrı static func notifyDataChanged() { Task { @MainActor in refreshAllWidgets() } } } // 2. Görünüm karmaşıklığını sınırla struct OptimizedWidgetView: View { let entry: TaskEntry var body: some View { // GeometryReader olmadan basit görünümleri tercih edin VStack(alignment: .leading, spacing: 8) { ForEach(entry.tasks.prefix(3)) { task in // Hafif bileşenler minimalTaskRow(task) } } .padding() } // Performans için minimal görünüm @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. Basit durumlar için @AppStorage kullan struct QuickSettingsWidgetView: View { // Paylaşılan UserDefaults'a doğrudan erişim @AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app")) private var showCompleted = true var body: some View { // Durum yenilemeler arasında kalıcıdır Text(showCompleted ? "Tümünü gösteriyor" : "Tamamlananları gizliyor") } } // 4. Veriyi provider'da önceden hesapla struct OptimizedTaskEntry: TimelineEntry { let date: Date let tasks: [Task] // Önceden hesaplanmış veri let completedCount: Int let pendingCount: Int let highPriorityCount: Int init(date: Date, tasks: [Task]) { self.date = date self.tasks = tasks // Hesaplamalar bir kez yapılır 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. ## Sonuç App Intents ile WidgetKit iOS 17+, widget'ları iOS uygulamalarının gerçek etkileşimli uzantılarına dönüştürür. Bu deklaratif mimari, native ve akıcı bir kullanıcı deneyimi sunarken geliştirmeyi önemli ölçüde basitleştirir. ### iOS 17+ Etkileşimli Widget Kontrol Listesi - ✅ Veri paylaşımı için App Group yapılandırın - ✅ Uygun yenileme ile Timeline Provider oluşturun - ✅ Her eylem için App Intents uygulayın - ✅ Etkileşim için `Button(intent:)` veya `Toggle(intent:)` kullanın - ✅ Modifikasyondan sonra `WidgetCenter.shared.reloadTimelines` çağırın - ✅ iOS 17+ için zorunlu `.containerBackground` ekleyin - ✅ Akıcı geçiş animasyonları uygulayın - ✅ Yükleme ve hata durumlarını yönetin - ✅ Görünümleri batarya performansı için optimize edin - ✅ Tüm desteklenen widget boyutlarında test edin --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/tr/blog/ios/widgetkit-ios17-interactive-widgets-app-intents