WidgetKit iOS 17+: Interaktywne Widgety z App Intents

Kompletny przewodnik tworzenia interaktywnych widgetów iOS z WidgetKit i App Intents. Przyciski, przełączniki, animacje i najlepsze praktyki dla iOS 17+ w 2026 roku.

WidgetKit iOS 17+ z interaktywnymi widgetami i App Intents dla nowoczesnych aplikacji iOS

iOS 17 zrewolucjonował WidgetKit, wprowadzając natywną interaktywność. Widgety nie są już statycznymi wyświetleniami: teraz mogą reagować na działania użytkownika bezpośrednio z ekranu głównego, bez otwierania aplikacji. Ta ważna ewolucja opiera się na frameworku App Intents i zapewnia płynne, nowoczesne doświadczenie użytkownika.

Co obejmuje ten artykuł

Artykuł przedstawia kompletne tworzenie interaktywnych widgetów iOS 17+, od konfiguracji projektu po zaawansowane wzorce z animacjami i zarządzaniem stanem.

Architektura Interaktywnych Widgetów

Interaktywność widgetów iOS 17+ działa poprzez framework App Intents. W przeciwieństwie do tradycyjnych deep linków, które otwierałyby aplikację, App Intents pozwalają wykonywać kod bezpośrednio z widgetu, a następnie automatycznie odświeżać wyświetlanie z nowymi danymi.

InteractiveWidgetArchitecture.swiftswift
import WidgetKit
import SwiftUI
import AppIntents

// Architektura opiera się na trzech głównych komponentach:
// 1. Widget Timeline Provider - dostarcza dane
// 2. Widget View - wyświetla interfejs z Button/Toggle
// 3. App Intent - wykonuje akcję po dotknięciu

struct TaskWidget: Widget {
    // Unikalny identyfikator widgetu
    let kind: String = "TaskWidget"

    var body: some WidgetConfiguration {
        // StaticConfiguration dla widgetów bez parametrów
        StaticConfiguration(
            kind: kind,
            provider: TaskTimelineProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
                // Wymagane dla App Intents
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Zadania")
        .description("Zarządzaj swoimi zadaniami z ekranu głównego.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

Widget deklaruje swoją konfigurację i określa providera, który będzie dostarczać dane. Atrybut .containerBackground jest obowiązkowy od iOS 17 dla interaktywnych widgetów.

Tworzenie Timeline Provider

Timeline Provider określa, kiedy i jak widget jest odświeżany. Dla interaktywnych widgetów musi również reagować na zmiany powodowane przez App Intents.

TaskTimelineProvider.swiftswift
import WidgetKit
import SwiftUI

// Entry reprezentujący stan widgetu w danej chwili
struct TaskEntry: TimelineEntry {
    let date: Date
    let tasks: [Task]

    // Stan ładowania dla wizualnej informacji zwrotnej
    var isLoading: Bool = false
}

// Model danych współdzielony między aplikacją a widgetem
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 wyświetlany podczas początkowego ładowania
    func placeholder(in context: Context) -> TaskEntry {
        TaskEntry(
            date: Date(),
            tasks: [
                Task(id: UUID(), title: "Przykładowe zadanie", isCompleted: false, priority: .medium)
            ]
        )
    }

    // Snapshot dla galerii widgetów
    func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
        let entry = TaskEntry(
            date: Date(),
            tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { $0 }
        )
        completion(entry)
    }

    // Pełna timeline z polityką odświeżania
    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)))

        // Odświeżenie za 15 minut lub po akcji użytkownika
        let nextUpdate = Calendar.current.date(
            byAdding: .minute,
            value: 15,
            to: Date()
        ) ?? Date()

        let timeline = Timeline(
            entries: [entry],
            policy: .after(nextUpdate)
        )
        completion(timeline)
    }
}

Provider używa współdzielonego TaskDataManager do dostępu do danych. To podejście gwarantuje synchronizację między główną aplikacją a widgetem.

App Group wymagane

Aby współdzielić dane między aplikacją a widgetem, trzeba skonfigurować App Group w capabilities projektu. UserDefaults lub pliki muszą używać tej współdzielonej grupy.

Menedżer Współdzielonych Danych

Współdzielenie danych między aplikacją a widgetem wymaga wspólnego kontenera dostępnego przez App Group.

TaskDataManager.swiftswift
import Foundation

final class TaskDataManager {
    // Singleton dla globalnego dostępu
    static let shared = TaskDataManager()

    // Identyfikator App Group skonfigurowany w Xcode
    private let appGroupID = "group.com.example.taskapp"

    // UserDefaults współdzielony między aplikacją a widgetem
    private var sharedDefaults: UserDefaults? {
        UserDefaults(suiteName: appGroupID)
    }

    private let tasksKey = "tasks"

    private init() {}

    // Pobiera zadania ze współdzielonego magazynu
    func fetchTasks() -> [Task] {
        guard let data = sharedDefaults?.data(forKey: tasksKey),
              let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
            return []
        }
        return tasks
    }

    // Zapisuje z powiadomieniem widgetu
    func saveTasks(_ tasks: [Task]) {
        guard let data = try? JSONEncoder().encode(tasks) else { return }
        sharedDefaults?.set(data, forKey: tasksKey)
    }

    // Aktualizuje konkretne zadanie
    func updateTask(_ task: Task) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks[index] = task
            saveTasks(tasks)
        }
    }

    // Przełącza stan ukończenia
    func toggleTaskCompletion(taskID: UUID) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { $0.id == taskID }) {
            tasks[index].isCompleted.toggle()
            saveTasks(tasks)
        }
    }
}

Menedżer ten enkapsuluje całą logikę persystencji i będzie używany zarówno przez aplikację, jak i App Intents widgetu.

Tworzenie App Intent dla Interaktywności

App Intent definiuje akcję wykonywaną gdy użytkownik wchodzi w interakcję z widgetem. iOS wykonuje tę akcję w tle, a następnie automatycznie odświeża widget.

ToggleTaskIntent.swiftswift
import AppIntents
import WidgetKit

// Intent do przełączania stanu zadania
struct ToggleTaskIntent: AppIntent {
    // Tytuł wyświetlany w skrótach Siri
    static var title: LocalizedStringResource = "Przełącz stan zadania"

    // Opis dla dostępności
    static var description = IntentDescription("Oznacza zadanie jako ukończone lub nieukończone.")

    // Parametr: ID zadania do modyfikacji
    @Parameter(title: "ID zadania")
    var taskID: String

    // Wymagany inicjalizator dla AppIntent
    init() {}

    // Inicjalizator z parametrem do tworzenia z widoku
    init(taskID: UUID) {
        self.taskID = taskID.uuidString
    }

    // Wykonanie akcji
    func perform() async throws -> some IntentResult {
        // Konwersja ID string na UUID
        guard let uuid = UUID(uuidString: taskID) else {
            return .result()
        }

        // Aktualizacja zadania
        TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)

        // Żąda odświeżenia widgetu
        WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")

        return .result()
    }
}

Wywołanie WidgetCenter.shared.reloadTimelines powoduje natychmiastowe odświeżenie widgetu po akcji, gwarantując natychmiastową wizualną informację zwrotną.

Gotowy na rozmowy o iOS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Widok Widgetu z Interaktywnymi Przyciskami

Widok widgetu używa standardowego komponentu SwiftUI Button z intentem jako akcją. iOS 17+ automatycznie przechwytuje te interakcje, aby wykonać App Intent.

TaskWidgetView.swiftswift
import SwiftUI
import WidgetKit

struct TaskWidgetView: View {
    let entry: TaskEntry

    // Adaptacja do rozmiaru widgetu
    @Environment(\.widgetFamily) var family

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // Nagłówek z tytułem i licznikiem
            headerView

            // Lista zadań z interaktywnymi przyciskami
            ForEach(entry.tasks.prefix(tasksLimit)) { task in
                TaskRowView(task: task)
            }

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

    // Liczba zadań w zależności od rozmiaru
    private var tasksLimit: Int {
        switch family {
        case .systemSmall: return 2
        case .systemMedium: return 3
        default: return 4
        }
    }

    private var headerView: some View {
        HStack {
            Text("Zadania")
                .font(.headline)
                .fontWeight(.bold)

            Spacer()

            // Plakietka z liczbą pozostałych zadań
            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 {
        // Przycisk z App Intent jako akcją
        Button(intent: ToggleTaskIntent(taskID: task.id)) {
            HStack(spacing: 12) {
                // Wskaźnik ukończenia
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title3)
                    .foregroundStyle(task.isCompleted ? .green : .secondary)

                // Tytuł zadania
                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
                    .lineLimit(1)

                Spacer()

                // Wskaźnik priorytetu
                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()
        }
    }
}

Składnia Button(intent:) bezpośrednio łączy przycisk z App Intent. Po dotknięciu iOS wykonuje perform(), a następnie automatycznie odświeża widget.

Interaktywny Toggle dla Widgetów

Dla akcji typu włącz/wyłącz komponent Toggle oferuje alternatywę dla przycisku z natywnym stylem iOS.

ToggleWidgetView.swiftswift
import SwiftUI
import AppIntents

// Specyficzny intent dla Toggle z jawnym stanem
struct SetTaskCompletionIntent: AppIntent {
    static var title: LocalizedStringResource = "Ustaw stan zadania"

    @Parameter(title: "ID zadania")
    var taskID: String

    // Stan docelowy: true = ukończone, false = nieukończone
    @Parameter(title: "Ukończone")
    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 }) {
            // Ustawia stan jawnie (nie 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()

            // Interaktywny toggle z intentem
            Toggle(
                isOn: task.isCompleted,
                intent: SetTaskCompletionIntent(
                    taskID: task.id,
                    isCompleted: !task.isCompleted
                )
            )
            .toggleStyle(.switch)
            .labelsHidden()
        }
        .padding(.vertical, 4)
    }
}

Toggle zapewnia bardziej intuicyjną interakcję dla stanów binarnych i naturalnie integruje się z designem iOS.

Ograniczenia interaktywnych widgetów

Widgety nie mogą wyświetlać alertów, sheet ani nawigacji. Wszystkie akcje muszą być autonomiczne i bezpośrednio aktualizować widoczny stan.

Animacje Odświeżania i Przejścia

iOS 17+ pozwala animować przejścia podczas odświeżania widgetu po akcji. Modyfikator .contentTransition kontroluje te animacje.

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) {
                // Ikona z animacją przejścia
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title3)
                    .foregroundStyle(task.isCompleted ? .green : .secondary)
                    // Animacja ikony przy zmianie
                    .contentTransition(.symbolEffect(.replace))

                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
                    // Animacja tekstu
                    .contentTransition(.opacity)

                Spacer()
            }
            .padding(.vertical, 6)
            .padding(.horizontal, 10)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear)
            )
            // Animacja tła
            .animation(.easeInOut(duration: 0.3), value: task.isCompleted)
        }
        .buttonStyle(.plain)
    }
}

// Widget z animowaną inwalidacją
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("Animowane Zadania")
        .description("Widgety z płynnymi animacjami.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
        // Aktywacja animacji zawartości
        .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("Zadania")
                .font(.headline.bold())

            Spacer()

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

            // Animowany postęp
            Text("\(completed)/\(total)")
                .font(.caption.bold())
                .foregroundStyle(.secondary)
                .contentTransition(.numericText())
        }
    }
}

Animacje .symbolEffect(.replace) i .numericText() tworzą płynne przejścia między stanami, znacząco poprawiając doświadczenie użytkownika.

Konfigurowalny Widget z AppIntentConfiguration

Dla widgetów konfigurowalnych przez użytkownika (filtry, kategorie) AppIntentConfiguration zastępuje StaticConfiguration.

ConfigurableTaskWidget.swiftswift
import WidgetKit
import SwiftUI
import AppIntents

// Konfiguracja udostępniona użytkownikowi
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Konfiguracja zadań"
    static var description = IntentDescription("Dostosuj wyświetlanie zadań.")

    // Filtr według priorytetu
    @Parameter(title: "Priorytet", default: .all)
    var priorityFilter: PriorityFilter

    // Pokaż ukończone zadania
    @Parameter(title: "Pokaż ukończone", default: true)
    var showCompleted: Bool

    // Maksymalna liczba zadań
    @Parameter(title: "Liczba zadań", default: 3)
    var maxTasks: Int
}

// Enum dla filtra priorytetu
enum PriorityFilter: String, AppEnum {
    case all
    case high
    case medium
    case low

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Priorytet"

    static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
        .all: "Wszystkie",
        .high: "Wysoki",
        .medium: "Średni",
        .low: "Niski"
    ]
}

// Provider dostosowany do konfiguracji
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))
    }

    // Stosuje filtry konfiguracyjne
    private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
        var tasks = TaskDataManager.shared.fetchTasks()

        // Filtrowanie według priorytetu
        if config.priorityFilter != .all {
            let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
            tasks = tasks.filter { $0.priority == priority }
        }

        // Filtrowanie ukończonych w razie potrzeby
        if !config.showCompleted {
            tasks = tasks.filter { !$0.isCompleted }
        }

        // Ograniczenie liczby
        return Array(tasks.prefix(config.maxTasks))
    }
}

// Widget z konfiguracją użytkownika
struct ConfigurableTaskWidget: Widget {
    let kind: String = "ConfigurableTaskWidget"

    var body: some WidgetConfiguration {
        // AppIntentConfiguration dla konfigurowalnych widgetów
        AppIntentConfiguration(
            kind: kind,
            intent: TaskWidgetConfigurationIntent.self,
            provider: ConfigurableTaskProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Spersonalizowane Zadania")
        .description("Filtruj i personalizuj swoje zadania.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

Użytkownik może teraz konfigurować widget poprzez długie naciśnięcie, otrzymując spersonalizowane doświadczenie bez dodatkowego kodu w aplikacji.

Obsługa Błędów i Stanów Ładowania

Dobre UX wymaga obsługi przypadków błędów i stanów pośrednich podczas interakcji.

TaskIntentWithFeedback.swiftswift
import AppIntents
import WidgetKit

struct ToggleTaskWithFeedbackIntent: AppIntent {
    static var title: LocalizedStringResource = "Przełącz zadanie z feedbackiem"

    @Parameter(title: "ID zadania")
    var taskID: String

    init() {}

    init(taskID: UUID) {
        self.taskID = taskID.uuidString
    }

    func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
        guard let uuid = UUID(uuidString: taskID) else {
            // Zwraca cichy błąd
            return .result(value: false)
        }

        // Symuluje operację asynchroniczną (np. sync z serwerem)
        do {
            try await Task.sleep(for: .milliseconds(100))

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

            return .result(value: true)
        } catch {
            // Błąd: nie aktualizuj widgetu
            return .result(value: false)
        }
    }
}

// Widok ze stanem ładowania
struct TaskRowWithLoadingView: View {
    let task: Task
    @State private var isLoading = false

    var body: some View {
        Button(intent: ToggleTaskIntent(taskID: task.id)) {
            HStack(spacing: 12) {
                // Wskaźnik warunkowy
                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)
    }
}

Natychmiastowa wizualna informacja zwrotna (zmniejszona przezroczystość, wskaźnik ładowania) informuje użytkownika, że jego akcja została zarejestrowana.

Najlepsze Praktyki i Optymalizacje

Kilka wzorców gwarantuje wydajne i niezawodne interaktywne widgety.

WidgetBestPractices.swiftswift
import WidgetKit
import SwiftUI

// 1. Zawsze unieważniaj cache po modyfikacji
final class WidgetRefreshManager {
    static func refreshAllWidgets() {
        // Odświeżenie wszystkich widgetów aplikacji
        WidgetCenter.shared.reloadAllTimelines()
    }

    static func refreshWidget(kind: String) {
        // Odświeżenie konkretnego widgetu
        WidgetCenter.shared.reloadTimelines(ofKind: kind)
    }

    // Wywołanie z aplikacji po modyfikacji danych
    static func notifyDataChanged() {
        Task { @MainActor in
            refreshAllWidgets()
        }
    }
}

// 2. Ogranicz złożoność widoków
struct OptimizedWidgetView: View {
    let entry: TaskEntry

    var body: some View {
        // Preferuj proste widoki bez GeometryReader
        VStack(alignment: .leading, spacing: 8) {
            ForEach(entry.tasks.prefix(3)) { task in
                // Lekkie komponenty
                minimalTaskRow(task)
            }
        }
        .padding()
    }

    // Minimalny widok dla wydajności
    @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. Używaj @AppStorage dla prostych stanów
struct QuickSettingsWidgetView: View {
    // Bezpośredni dostęp do współdzielonych UserDefaults
    @AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
    private var showCompleted = true

    var body: some View {
        // Stan utrzymuje się między odświeżeniami
        Text(showCompleted ? "Pokazuję wszystkie" : "Ukrywam ukończone")
    }
}

// 4. Wstępnie obliczaj dane w providerze
struct OptimizedTaskEntry: TimelineEntry {
    let date: Date
    let tasks: [Task]

    // Wstępnie obliczone dane
    let completedCount: Int
    let pendingCount: Int
    let highPriorityCount: Int

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

        // Obliczenia wykonywane raz
        self.completedCount = tasks.filter(\.isCompleted).count
        self.pendingCount = tasks.filter { !$0.isCompleted }.count
        self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.isCompleted }.count
    }
}

Te optymalizacje gwarantują responsywne widgety, które nie zużywają nadmiernie baterii.

Debugowanie widgetów

Używaj schematu widget w Xcode do debugowania. Podgląd canvas pozwala testować różne rozmiary i stany bez instalacji na urządzeniu.

Podsumowanie

WidgetKit iOS 17+ z App Intents przekształca widgety w prawdziwe interaktywne rozszerzenia aplikacji iOS. Ta deklaratywna architektura znacząco upraszcza rozwój, oferując natywne i płynne doświadczenie użytkownika.

Lista Kontrolna Interaktywnego Widgetu iOS 17+

  • ✅ Skonfiguruj App Group dla współdzielenia danych
  • ✅ Stwórz Timeline Provider z odpowiednim odświeżaniem
  • ✅ Zaimplementuj App Intents dla każdej akcji
  • ✅ Używaj Button(intent:) lub Toggle(intent:) dla interaktywności
  • ✅ Wywołuj WidgetCenter.shared.reloadTimelines po modyfikacji
  • ✅ Dodaj obowiązkowy .containerBackground dla iOS 17+
  • ✅ Zaimplementuj płynne animacje przejścia
  • ✅ Obsłuż stany ładowania i błędów
  • ✅ Optymalizuj widoki pod kątem wydajności baterii
  • ✅ Testuj na wszystkich obsługiwanych rozmiarach widgetu

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

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

Udostępnij

Powiązane artykuły