WidgetKit iOS 17+ : créer des widgets interactifs avec App Intents

Guide complet pour créer des widgets iOS interactifs avec WidgetKit et App Intents. Boutons, toggles, animations et bonnes pratiques pour iOS 17+ en 2026.

WidgetKit iOS 17+ avec widgets interactifs et App Intents pour applications iOS modernes

iOS 17 a révolutionné WidgetKit en introduisant l'interactivité native. Les widgets ne sont plus de simples affichages statiques : ils peuvent désormais réagir aux actions utilisateur directement sur l'écran d'accueil, sans ouvrir l'application. Cette évolution majeure repose sur le framework App Intents, offrant une expérience utilisateur fluide et moderne.

Ce que couvre cet article

Cet article présente la création complète de widgets interactifs iOS 17+, depuis la configuration du projet jusqu'aux patterns avancés avec animations et gestion d'état.

Architecture des widgets interactifs

L'interactivité des widgets iOS 17+ fonctionne via le framework App Intents. Contrairement aux deep links traditionnels qui ouvraient l'app, les App Intents permettent d'exécuter du code directement depuis le widget, puis de rafraîchir l'affichage avec les nouvelles données.

InteractiveWidgetArchitecture.swiftswift
import WidgetKit
import SwiftUI
import AppIntents

// L'architecture repose sur trois composants principaux :
// 1. Widget Timeline Provider - fournit les données
// 2. Widget View - affiche l'interface avec Button/Toggle
// 3. App Intent - exécute l'action au tap

struct TaskWidget: Widget {
    // Identifiant unique du widget
    let kind: String = "TaskWidget"

    var body: some WidgetConfiguration {
        // StaticConfiguration pour widgets sans paramètres
        StaticConfiguration(
            kind: kind,
            provider: TaskTimelineProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
                // Requis pour App Intents
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Tâches")
        .description("Gérez vos tâches depuis l'écran d'accueil.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

Le widget déclare sa configuration et spécifie le provider qui fournira les données. L'attribut .containerBackground est obligatoire depuis iOS 17 pour les widgets interactifs.

Création du Timeline Provider

Le Timeline Provider détermine quand et comment le widget se rafraîchit. Pour les widgets interactifs, il doit également réagir aux changements déclenchés par les App Intents.

TaskTimelineProvider.swiftswift
import WidgetKit
import SwiftUI

// Entry représentant l'état du widget à un instant donné
struct TaskEntry: TimelineEntry {
    let date: Date
    let tasks: [Task]

    // État de chargement pour feedback visuel
    var isLoading: Bool = false
}

// Modèle de données partagé entre app et 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 affiché pendant le chargement initial
    func placeholder(in context: Context) -> TaskEntry {
        TaskEntry(
            date: Date(),
            tasks: [
                Task(id: UUID(), title: "Exemple de tâche", isCompleted: false, priority: .medium)
            ]
        )
    }

    // Snapshot pour la galerie de widgets
    func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
        let entry = TaskEntry(
            date: Date(),
            tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { $0 }
        )
        completion(entry)
    }

    // Timeline complète avec politique de rafraîchissement
    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)))

        // Rafraîchissement dans 15 minutes ou après action utilisateur
        let nextUpdate = Calendar.current.date(
            byAdding: .minute,
            value: 15,
            to: Date()
        ) ?? Date()

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

Le provider utilise un TaskDataManager partagé pour accéder aux données. Cette approche garantit la synchronisation entre l'application principale et le widget.

App Group requis

Pour partager des données entre l'app et le widget, configurez un App Group dans les capabilities du projet. Le UserDefaults ou les fichiers doivent utiliser ce groupe partagé.

Gestionnaire de données partagé

Le partage de données entre l'application et le widget nécessite un conteneur commun accessible via App Group.

TaskDataManager.swiftswift
import Foundation

final class TaskDataManager {
    // Singleton pour accès global
    static let shared = TaskDataManager()

    // Identifiant de l'App Group configuré dans Xcode
    private let appGroupID = "group.com.example.taskapp"

    // UserDefaults partagé entre app et widget
    private var sharedDefaults: UserDefaults? {
        UserDefaults(suiteName: appGroupID)
    }

    private let tasksKey = "tasks"

    private init() {}

    // Récupération des tâches depuis le stockage partagé
    func fetchTasks() -> [Task] {
        guard let data = sharedDefaults?.data(forKey: tasksKey),
              let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
            return []
        }
        return tasks
    }

    // Sauvegarde avec notification au widget
    func saveTasks(_ tasks: [Task]) {
        guard let data = try? JSONEncoder().encode(tasks) else { return }
        sharedDefaults?.set(data, forKey: tasksKey)
    }

    // Mise à jour d'une tâche spécifique
    func updateTask(_ task: Task) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks[index] = task
            saveTasks(tasks)
        }
    }

    // Basculement de l'état completed
    func toggleTaskCompletion(taskID: UUID) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { $0.id == taskID }) {
            tasks[index].isCompleted.toggle()
            saveTasks(tasks)
        }
    }
}

Ce gestionnaire encapsule toute la logique de persistance et sera utilisé à la fois par l'application et les App Intents du widget.

Création de l'App Intent pour l'interactivité

L'App Intent définit l'action exécutée lorsque l'utilisateur interagit avec le widget. iOS exécute cette action en arrière-plan puis rafraîchit automatiquement le widget.

ToggleTaskIntent.swiftswift
import AppIntents
import WidgetKit

// Intent pour basculer l'état d'une tâche
struct ToggleTaskIntent: AppIntent {
    // Titre affiché dans les raccourcis Siri
    static var title: LocalizedStringResource = "Basculer l'état d'une tâche"

    // Description pour l'accessibilité
    static var description = IntentDescription("Marque une tâche comme complétée ou non complétée.")

    // Paramètre : l'ID de la tâche à modifier
    @Parameter(title: "ID de la tâche")
    var taskID: String

    // Initializer requis pour AppIntent
    init() {}

    // Initializer avec paramètre pour création depuis la vue
    init(taskID: UUID) {
        self.taskID = taskID.uuidString
    }

    // Exécution de l'action
    func perform() async throws -> some IntentResult {
        // Conversion de l'ID string vers UUID
        guard let uuid = UUID(uuidString: taskID) else {
            return .result()
        }

        // Mise à jour de la tâche
        TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)

        // Demande de rafraîchissement du widget
        WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")

        return .result()
    }
}

L'appel à WidgetCenter.shared.reloadTimelines déclenche le rafraîchissement immédiat du widget après l'action, assurant un feedback visuel instantané.

Prêt à réussir tes entretiens iOS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Vue du widget avec boutons interactifs

La vue du widget utilise le composant Button standard de SwiftUI avec l'intent comme action. iOS 17+ intercepte automatiquement ces interactions pour exécuter l'App Intent.

TaskWidgetView.swiftswift
import SwiftUI
import WidgetKit

struct TaskWidgetView: View {
    let entry: TaskEntry

    // Adaptation à la taille du widget
    @Environment(\.widgetFamily) var family

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // Header avec titre et compteur
            headerView

            // Liste des tâches avec boutons interactifs
            ForEach(entry.tasks.prefix(tasksLimit)) { task in
                TaskRowView(task: task)
            }

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

    // Nombre de tâches selon la taille
    private var tasksLimit: Int {
        switch family {
        case .systemSmall: return 2
        case .systemMedium: return 3
        default: return 4
        }
    }

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

            Spacer()

            // Badge avec le nombre de tâches restantes
            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 avec App Intent comme action
        Button(intent: ToggleTaskIntent(taskID: task.id)) {
            HStack(spacing: 12) {
                // Indicateur de complétion
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title3)
                    .foregroundStyle(task.isCompleted ? .green : .secondary)

                // Titre de la tâche
                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
                    .lineLimit(1)

                Spacer()

                // Indicateur de priorité
                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()
        }
    }
}

La syntaxe Button(intent:) connecte directement le bouton à l'App Intent. Au tap, iOS exécute perform() puis rafraîchit le widget automatiquement.

Toggle interactif pour widgets

Pour les actions de type on/off, le composant Toggle offre une alternative au bouton avec un style natif iOS.

ToggleWidgetView.swiftswift
import SwiftUI
import AppIntents

// Intent spécifique pour Toggle avec état explicite
struct SetTaskCompletionIntent: AppIntent {
    static var title: LocalizedStringResource = "Définir l'état de la tâche"

    @Parameter(title: "ID de la tâche")
    var taskID: String

    // État cible : true = complété, false = non complété
    @Parameter(title: "Complété")
    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 }) {
            // Définit l'état explicitement (pas toggle)
            tasks[index].isCompleted = isCompleted
            TaskDataManager.shared.saveTasks(tasks)
        }

        WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
        return .result()
    }
}

struct TaskToggleRowView: View {
    let task: Task

    var body: some View {
        HStack {
            Text(task.title)
                .font(.subheadline)
                .strikethrough(task.isCompleted)

            Spacer()

            // Toggle interactif avec intent
            Toggle(
                isOn: task.isCompleted,
                intent: SetTaskCompletionIntent(
                    taskID: task.id,
                    isCompleted: !task.isCompleted
                )
            )
            .toggleStyle(.switch)
            .labelsHidden()
        }
        .padding(.vertical, 4)
    }
}

Le Toggle offre une interaction plus intuitive pour les états binaires et s'intègre naturellement dans les designs iOS.

Limites des widgets interactifs

Les widgets ne peuvent pas afficher d'alertes, de sheets ou de navigation. Toutes les actions doivent être autonomes et mettre à jour l'état visible directement.

Animations et transitions au rafraîchissement

iOS 17+ permet d'animer les transitions lors du rafraîchissement du widget après une action. Le modificateur .contentTransition contrôle ces animations.

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) {
                // Icône avec animation de transition
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title3)
                    .foregroundStyle(task.isCompleted ? .green : .secondary)
                    // Animation de l'icône au changement
                    .contentTransition(.symbolEffect(.replace))

                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
                    // Animation du texte
                    .contentTransition(.opacity)

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

// Widget avec invalidation animée
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("Tâches animées")
        .description("Widgets avec animations fluides.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
        // Active les animations de contenu
        .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("Tâches")
                .font(.headline.bold())

            Spacer()

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

            // Progression animée
            Text("\(completed)/\(total)")
                .font(.caption.bold())
                .foregroundStyle(.secondary)
                .contentTransition(.numericText())
        }
    }
}

Les animations .symbolEffect(.replace) et .numericText() créent des transitions fluides entre les états, améliorant significativement l'expérience utilisateur.

Widget configurable avec AppIntentConfiguration

Pour les widgets personnalisables par l'utilisateur (filtres, catégories), AppIntentConfiguration remplace StaticConfiguration.

ConfigurableTaskWidget.swiftswift
import WidgetKit
import SwiftUI
import AppIntents

// Configuration exposée à l'utilisateur
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Configuration des tâches"
    static var description = IntentDescription("Personnalisez l'affichage des tâches.")

    // Filtre par priorité
    @Parameter(title: "Priorité", default: .all)
    var priorityFilter: PriorityFilter

    // Afficher les tâches complétées
    @Parameter(title: "Afficher complétées", default: true)
    var showCompleted: Bool

    // Nombre maximum de tâches
    @Parameter(title: "Nombre de tâches", default: 3)
    var maxTasks: Int
}

// Enum pour le filtre de priorité
enum PriorityFilter: String, AppEnum {
    case all
    case high
    case medium
    case low

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Priorité"

    static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
        .all: "Toutes",
        .high: "Haute",
        .medium: "Moyenne",
        .low: "Basse"
    ]
}

// Provider adapté à la 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))
    }

    // Applique les filtres de configuration
    private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
        var tasks = TaskDataManager.shared.fetchTasks()

        // Filtre par priorité
        if config.priorityFilter != .all {
            let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
            tasks = tasks.filter { $0.priority == priority }
        }

        // Filtre les complétées si nécessaire
        if !config.showCompleted {
            tasks = tasks.filter { !$0.isCompleted }
        }

        // Limite le nombre
        return Array(tasks.prefix(config.maxTasks))
    }
}

// Widget avec configuration utilisateur
struct ConfigurableTaskWidget: Widget {
    let kind: String = "ConfigurableTaskWidget"

    var body: some WidgetConfiguration {
        // AppIntentConfiguration pour widgets configurables
        AppIntentConfiguration(
            kind: kind,
            intent: TaskWidgetConfigurationIntent.self,
            provider: ConfigurableTaskProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Tâches personnalisées")
        .description("Filtrez et personnalisez vos tâches.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

Les utilisateurs peuvent maintenant configurer le widget via un long press, offrant une expérience personnalisée sans code additionnel dans l'application.

Gestion des erreurs et états de chargement

Une bonne UX nécessite de gérer les cas d'erreur et les états intermédiaires lors des interactions.

TaskIntentWithFeedback.swiftswift
import AppIntents
import WidgetKit

struct ToggleTaskWithFeedbackIntent: AppIntent {
    static var title: LocalizedStringResource = "Basculer tâche avec feedback"

    @Parameter(title: "ID de la tâche")
    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 {
            // Retourne un échec silencieux
            return .result(value: false)
        }

        // Simulation d'opération async (sync avec serveur par ex.)
        do {
            try await Task.sleep(for: .milliseconds(100))

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

            return .result(value: true)
        } catch {
            // Erreur : ne pas mettre à jour le widget
            return .result(value: false)
        }
    }
}

// Vue avec état de chargement
struct TaskRowWithLoadingView: View {
    let task: Task
    @State private var isLoading = false

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

Le feedback visuel immédiat (opacité réduite, indicateur de chargement) informe l'utilisateur que son action a été prise en compte.

Bonnes pratiques et optimisations

Plusieurs patterns garantissent des widgets interactifs performants et fiables.

WidgetBestPractices.swiftswift
import WidgetKit
import SwiftUI

// 1. Toujours invalider le cache après modification
final class WidgetRefreshManager {
    static func refreshAllWidgets() {
        // Rafraîchit tous les widgets de l'app
        WidgetCenter.shared.reloadAllTimelines()
    }

    static func refreshWidget(kind: String) {
        // Rafraîchit un widget spécifique
        WidgetCenter.shared.reloadTimelines(ofKind: kind)
    }

    // Appeler depuis l'app après modification de données
    static func notifyDataChanged() {
        Task { @MainActor in
            refreshAllWidgets()
        }
    }
}

// 2. Limiter la complexité des vues
struct OptimizedWidgetView: View {
    let entry: TaskEntry

    var body: some View {
        // Préférer les vues simples sans GeometryReader
        VStack(alignment: .leading, spacing: 8) {
            ForEach(entry.tasks.prefix(3)) { task in
                // Composants légers
                minimalTaskRow(task)
            }
        }
        .padding()
    }

    // Vue minimale pour 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. Utiliser @AppStorage pour état simple
struct QuickSettingsWidgetView: View {
    // Accès direct aux UserDefaults partagés
    @AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
    private var showCompleted = true

    var body: some View {
        // L'état persiste entre les rafraîchissements
        Text(showCompleted ? "Showing all" : "Hiding completed")
    }
}

// 4. Pré-calculer les données dans le provider
struct OptimizedTaskEntry: TimelineEntry {
    let date: Date
    let tasks: [Task]

    // Données pré-calculées
    let completedCount: Int
    let pendingCount: Int
    let highPriorityCount: Int

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

        // Calculs effectués une seule fois
        self.completedCount = tasks.filter(\.isCompleted).count
        self.pendingCount = tasks.filter { !$0.isCompleted }.count
        self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.isCompleted }.count
    }
}

Ces optimisations garantissent des widgets réactifs qui ne consomment pas excessivement de batterie.

Debug des widgets

Utilisez le schéma de widget dans Xcode pour déboguer. Le canvas preview permet de tester les différentes tailles et états sans installer sur device.

Conclusion

WidgetKit iOS 17+ avec App Intents transforme les widgets en véritables extensions interactives des applications iOS. Cette architecture déclarative simplifie considérablement le développement tout en offrant une expérience utilisateur native et fluide.

Checklist widgets interactifs iOS 17+

  • ✅ Configurer l'App Group pour le partage de données
  • ✅ Créer le Timeline Provider avec rafraîchissement approprié
  • ✅ Implémenter les App Intents pour chaque action
  • ✅ Utiliser Button(intent:) ou Toggle(intent:) pour l'interactivité
  • ✅ Appeler WidgetCenter.shared.reloadTimelines après modification
  • ✅ Ajouter .containerBackground obligatoire iOS 17+
  • ✅ Implémenter des animations de transition fluides
  • ✅ Gérer les états de chargement et d'erreur
  • ✅ Optimiser les vues pour la performance batterie
  • ✅ Tester sur toutes les tailles de widgets supportées

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

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

Partager

Articles similaires