TipKit dans SwiftUI : le guide complet des tips contextuels (iOS 26)
TipKit est le framework Apple pour afficher des tips contextuels dans SwiftUI. Ce guide couvre l'intégration, les règles à base d'événements et de paramètres, TipGroup, la synchro CloudKit, la personnalisation avec TipViewStyle et le test dans Xcode sur iOS 26.
TipKit est le framework Apple, introduit avec iOS 17 et enrichi jusqu'à iOS 26, qui permet d'afficher des tips contextuels (de petites bulles d'aide non intrusives) pour guider vos utilisateurs vers les fonctionnalités clés de votre app SwiftUI. Il gère automatiquement les règles d'affichage, la fréquence, la persistance de l'état et la synchronisation entre appareils via CloudKit. Honnêtement, après l'avoir intégré dans plusieurs apps, c'est devenu mon réflexe dès qu'il faut signaler une nouvelle feature. Ce guide couvre l'installation, les règles à base d'événements et de paramètres, TipGroup, la personnalisation, les tests, et les spécificités multiplateforme.
TipKit se déclare via le protocole Tip ; on associe un TipView à n'importe quelle vue SwiftUI par popover ou inline.
Les macros @Parameter et #Rule déclenchent un tip selon l'état de l'app ; les événements se donnent via event.donate().
TipGroup (iOS 18+) orchestre plusieurs tips avec deux priorités : .ordered (séquence stricte) et .firstAvailable (le premier éligible).
La synchro CloudKit est facultative. On l'active via .cloudKitContainer(.automatic) dans Tips.configure().
Les utilitaires de test (showAllTipsForTesting, resetDatastore) doivent être appelés AVANT configure() et gardés derrière #if DEBUG.
TipKit fonctionne sur iOS, iPadOS, macOS, watchOS et visionOS 26, avec un rendu adapté à chaque plateforme.
Qu'est-ce que TipKit et à quoi ça sert ?
TipKit est le framework introduit lors de la WWDC 2023 (avec iOS 17) pour standardiser la manière dont les apps Apple présentent des tips en contexte. Avant TipKit, chaque équipe réinventait ses propres overlays d'onboarding, souvent fragiles, difficiles à traduire, sans mémoire persistante. Le framework centralise ces problématiques : chaque tip est un modèle de données typé, associé à des règles d'affichage déclaratives, et TipKit gère l'ensemble du cycle de vie (premier affichage, fréquence maximale, invalidation, persistance sur disque, et synchronisation optionnelle entre les appareils de l'utilisateur).
À qui ça sert ? Aux équipes produit qui veulent guider les nouveaux utilisateurs sans bloquer les experts avec des popups intrusifs, aux apps riches en fonctionnalités cachées (long press, gestes secondaires, menus contextuels), et à toute app qui déploie une nouvelle feature dans une update et souhaite la signaler exactement une fois. TipKit ne remplace pas un vrai onboarding scripté ; il complète la découverte progressive avec des indices ciblés, dans les vues où les utilisateurs se trouvent déjà.
Sous le capot, TipKit maintient un datastore local (un fichier SQLite dans le sandbox de votre app) qui enregistre quels tips ont été affichés, invalidés, ou dont la fréquence maximum est atteinte. Sur iOS 18 et versions ultérieures, ce datastore peut être répliqué dans CloudKit pour éviter qu'un utilisateur voie deux fois le même tip lorsqu'il installe votre app sur son iPad puis sur son Mac. Sur iOS 26, le framework reste API-compatible avec iOS 17 : votre code existant continue de fonctionner tel quel, avec quelques modernisations autour de TipGroup et de la synchronisation.
Comment intégrer TipKit dans une app SwiftUI ?
L'intégration prend trois étapes : ajouter le framework, déclarer votre premier tip, et configurer le datastore au démarrage. Bonne nouvelle, TipKit fait partie du SDK depuis Xcode 15. Aucun package externe n'est nécessaire, un simple import TipKit suffit dans tout fichier qui manipule des tips.
Ensuite, définissez un tip en adoptant le protocole Tip :
import TipKit
struct FavoriteButtonTip: Tip {
var title: Text {
Text("Ajoutez à vos favoris")
}
var message: Text? {
Text("Touchez l'étoile pour retrouver cet article plus tard.")
}
var image: Image? {
Image(systemName: "star.fill")
}
}
Puis affichez-le, soit en popover attaché à une vue, soit en inline dans le flux :
struct ArticleView: View {
private let favoriteTip = FavoriteButtonTip()
var body: some View {
VStack {
Button {
// Toggle favori
} label: {
Image(systemName: "star")
}
.popoverTip(favoriteTip)
// Ou en inline :
TipView(favoriteTip, arrowEdge: .bottom)
}
}
}
Enfin, configurez le datastore au lancement de l'app. C'est l'étape qui manque à beaucoup de tutoriels et qui explique pourquoi vos tips ne s'affichent jamais (je me suis fait avoir la première fois, je vous rassure) :
@main
struct MyApp: App {
init() {
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
displayFrequency(.immediate) est utile pendant le développement ; en production, préférez .daily ou .weekly pour éviter de saturer les utilisateurs avec des tips consécutifs.
Règles et paramètres : afficher un tip au bon moment
Le vrai pouvoir de TipKit tient dans les règles. Un tip pertinent n'apparaît pas au premier lancement, il attend le moment où l'utilisateur en a réellement besoin. TipKit propose deux types de règles déclaratives : les règles de paramètres (basées sur un état persisté) et les règles d'événements (basées sur des actions passées).
Une règle de paramètre repose sur la macro @Parameter, qui déclare une valeur observable automatiquement persistée par TipKit :
struct ProModeTip: Tip {
@Parameter static var openedSettings: Bool = false
var title: Text { Text("Activez le mode Pro") }
var message: Text? { Text("Débloquez les filtres avancés depuis Réglages.") }
var rules: [Rule] {
[
#Rule(Self.$openedSettings) { $0 == true }
]
}
}
Dès que vous mettez à jour ProModeTip.openedSettings = true (typiquement quand l'utilisateur ouvre l'écran Réglages), TipKit réévalue la règle et le tip devient éligible à l'affichage.
Les macros #Rule acceptent n'importe quelle expression booléenne. Vous pouvez composer plusieurs paramètres, comparer à des seuils numériques, ou vérifier des chaînes. Combinez-les librement dans le tableau rules : toutes les règles doivent être vraies simultanément (comportement AND) pour que le tip soit éligible.
Comment déclencher un tip après un événement utilisateur ?
Les événements complètent les paramètres : au lieu de refléter un état permanent, ils comptent des occurrences. C'est le mécanisme idéal pour "affiche ce tip après que l'utilisateur ait ouvert un article trois fois" ou "après un premier partage réussi".
struct ShareTip: Tip {
static let didShare = Event(id: "didShare")
var title: Text { Text("Partagez avec vos amis") }
var message: Text? { Text("Envoyez un lien direct vers ce contenu.") }
var rules: [Rule] {
[
#Rule(Self.didShare) { $0.donations.count >= 3 }
]
}
}
Dans le code de votre bouton de partage, appelez donate() chaque fois que l'événement se produit :
La méthode donate() est asynchrone parce qu'elle écrit dans le datastore. TipKit conserve chaque donation avec sa date, donc vous pouvez aussi filtrer par récence : $0.donations.filter { $0.date > Date().addingTimeInterval(-3600) }.count >= 2 déclenchera le tip uniquement si l'utilisateur a partagé deux fois dans la dernière heure.
Les événements survivent aux redémarrages de l'app tant que le datastore n'est pas vidé. Ils sont aussi synchronisés via CloudKit si vous avez activé l'option, donc un utilisateur qui partage sur iPhone verra le tip s'afficher sur son iPad sans devoir refaire l'action.
Ordonner plusieurs tips avec TipGroup (iOS 18+)
Par défaut, TipKit n'impose aucun ordre entre plusieurs tips éligibles simultanément. C'est un problème classique quand vous ajoutez un tip par nouvelle feature au fil des versions (j'ai vu trois tips se superposer sur le même écran, du plus bel effet). TipGroup, introduit en iOS 18 et disponible sur iOS 26, résout ce problème avec deux stratégies de priorité :
Avec .ordered, TipKit affichera WelcomeTip d'abord ; seulement quand elle sera invalidée (touchée, fermée, ou expirée) il passera à FavoriteButtonTip, puis à ShareTip. C'est parfait pour un vrai parcours d'onboarding séquentiel réparti sur plusieurs écrans.
L'alternative .firstAvailable sélectionne le premier tip du groupe dont les règles sont vraies, sans ordre imposé. Utile quand vous avez trois tips indépendants dans la même vue et que vous voulez éviter de tous les afficher en même temps :
@State private var contextualTips = TipGroup(.firstAvailable) {
NewFilterTip()
KeyboardShortcutTip()
UndoGestureTip()
}
var body: some View {
ListView(...)
.popoverTip(contextualTips.currentTip)
}
contextualTips.currentTip est un Tip? optionnel : nil si aucun tip du groupe n'est éligible. Vous pouvez passer le même TipGroup à plusieurs vues pour un flow qui traverse NavigationStack sans perdre le fil (voir notre guide NavigationStack pour iOS 26 pour la partie navigation).
Personnaliser l'apparence avec TipViewStyle
Le rendu par défaut de TipKit s'aligne sur le design system Apple : couleurs système, corner radius standard, arrow qui pointe vers la vue attachée. Ça convient à 80 % des cas ; pour le reste, TipViewStyle permet un contrôle total sur la présentation.
L'objet Configuration expose title, message, image et tip. Vous les recomposez librement. Notez invalidate(reason:) : c'est le bon moyen de fermer manuellement un tip depuis un bouton custom, en enregistrant proprement la raison dans le datastore.
Sur iOS 26 avec Liquid Glass, background(.regularMaterial, ...) respecte automatiquement les surfaces translucides du nouveau design (voir notre guide Liquid Glass SwiftUI pour comprendre les matériaux). Le rendu se dégrade proprement sur les versions plus anciennes qui ne supportent pas les nouveaux blurs.
Synchroniser les tips avec CloudKit
Par défaut, chaque appareil garde son propre datastore. Un utilisateur peut donc voir le même tip trois fois s'il utilise votre app sur iPhone, iPad et Mac. Depuis iOS 18, .cloudKitContainer corrige ce comportement en répliquant l'état des tips à travers tous les appareils connectés au même compte iCloud.
Côté projet, activez les capabilities iCloud > CloudKit et Background Modes > Remote notifications dans la target Xcode. Sans le Background Mode, la synchro se fait uniquement au premier launch après une modification, ce qui produit un décalage frustrant en pratique (j'ai perdu une soirée à comprendre pourquoi ça ne se synchronisait pas en temps réel avant de tomber sur ce détail).
TipKit est disponible sur toutes les plateformes Apple modernes, mais le rendu n'est pas identique partout. C'est le genre de détail qui vous saute à la figure lors du QA cross-platform.
Sur iPadOS, les popover tips utilisent le vrai popover UIKit sous-jacent : ils flottent au-dessus des vues, avec l'arrow qui pointe vers la source. Sur iPad en Split View ou Slide Over, le popover se recentre automatiquement si l'espace disponible change. Vérifiez que votre vue source ne disparaît pas pendant l'affichage, sinon TipKit invalide le tip et l'utilisateur ne le reverra pas.
Sur macOS (avec Mac Catalyst ou SwiftUI natif), les popovers deviennent des NSPopover classiques. Les inline TipView se comportent comme sur iOS mais respectent la palette de couleurs macOS (labels plus discrets, backgrounds plus neutres). Le clic dehors ferme automatiquement le popover, comportement Mac-natif attendu par les utilisateurs.
Sur visionOS, les popovers apparaissent dans l'espace 3D à côté de leur source, avec la profondeur ajustée par le système. Aucun code spécifique n'est requis : .popoverTip() fonctionne exactement pareil, mais le rendu est translucide et sensible à l'orientation du regard de l'utilisateur.
Sur watchOS, seuls les inline TipView sont supportés (les popovers n'ont pas de sens sur un écran de 45 mm). Le rendu est ultra-condensé : évitez les messages longs, un titre plus une phrase courte est le maximum utilisable en pratique.
Si votre app cible plusieurs plateformes, testez chaque tip individuellement sur chaque cible plutôt que de faire confiance à l'auto-adaptation. Le contenu du texte, en particulier, ne réagit pas au Dynamic Type de la même façon selon la plateforme. Un tip parfait sur iPhone peut déborder sur Apple Watch ou paraître ridiculement petit sur un Studio Display.
Comment tester TipKit dans Xcode ?
Le comportement par défaut (n'afficher chaque tip qu'une fois par période) rend l'itération pénible. TipKit expose des utilitaires de test explicites pour reprendre la main pendant le développement :
Un bug connu sur iOS 18.4 et 18.5 (également reporté par plusieurs développeurs sur les forums Apple) : showAllTipsForTesting() peut relancer l'affichage toutes les 2 secondes environ, rendant l'écran illisible. Le workaround est de basculer sur showTipsForTesting([SpecificTip.self]) pendant vos sessions de debug. Le problème est corrigé sur iOS 26 selon les release notes.
Pièges courants et bonnes pratiques
Après une bonne dizaine d'apps intégrant TipKit sur iOS, iPadOS, macOS et visionOS, quelques leçons transversales méritent d'être notées avant de partir en production.
N'utilisez pas TipKit pour de l'information critique. Un tip peut ne jamais s'afficher : l'utilisateur a peut-être désactivé les tips système dans Réglages, ou vidé le storage manuellement. Si l'info est nécessaire pour utiliser une feature, faites-en un onboarding scripté avec NavigationStack et des écrans explicites, pas un tip.
Modélisez @Parameter comme un ViewModel léger. Si votre app utilise déjà le framework Observation avec @Observable, pensez à synchroniser vos paramètres TipKit avec votre state global. Sinon, ils divergent (par exemple l'utilisateur active une feature depuis Réglages, votre paramètre TipKit reste à false parce que vous avez oublié de le muter).
Testez les invalidations. Un tip qui n'a jamais été touché ne s'invalidera jamais avec .tipClosed. Écrivez des tests unitaires (avec Swift Testing par exemple) qui invoquent tip.invalidate(reason: .actionPerformed) et vérifient que le tip ne réapparaît plus sur les vues suivantes.
Localisez tôt. Les strings dans Text("...") sont automatiquement extractibles par xcstringstool, mais les tips sont vus par la totalité de votre base users. Un typo ou une string non traduite sera visible à grande échelle et immédiatement remonté par vos utilisateurs internationaux.
Limitez la fréquence..immediate en production produit une expérience de spam. .daily est un bon défaut ; .hourly uniquement pour les apps utilisées de façon très intensive, comme les clients email ou les éditeurs de code.
Appelez Tips.showAllTipsForTesting() avant Tips.configure(), ou ciblez un tip précis avec Tips.showTipsForTesting([MyTip.self]). Gardez toujours ces appels derrière #if DEBUG pour éviter qu'ils partent en production.
TipKit fonctionne-t-il sur macOS et visionOS ?
Oui, sur toutes les plateformes Apple modernes (iOS 17+, iPadOS 17+, macOS 14+, watchOS 10+, visionOS 1+). Les popovers deviennent NSPopover sur Mac et flottent en 3D sur visionOS avec la profondeur ajustée par le système.
Comment ordonner plusieurs tips séquentiellement ?
Utilisez TipGroup(.ordered) { Tip1(); Tip2() } introduit en iOS 18. Chaque tip ne s'affiche qu'après invalidation du précédent — parfait pour un onboarding séquentiel réparti sur plusieurs écrans.
Où est stocké l'état des tips ?
Dans un fichier SQLite dans le sandbox de l'app, sous Application Support par défaut. Avec .cloudKitContainer(.automatic), l'état se réplique aussi vers un conteneur CloudKit dédié se terminant par .tips.
Peut-on afficher un tip programmatiquement sans règles ?
Oui, un tip sans règles est immédiatement éligible dès son premier affichage. La macro #Rule est optionnelle ; sans elle, seuls displayFrequency, MaxDisplayCount et l'invalidation manuelle contrôlent le comportement.
Guide pratique NavigationStack en SwiftUI sous iOS 26 : NavigationPath, deep linking type-safe, accessibilité VoiceOver et migration depuis NavigationView, avec exemples de code Swift concrets et retours d'expérience.
Tout pour créer des Live Activities sur iOS 26 avec ActivityKit : configuration Xcode, ActivityAttributes, Dynamic Island, mises à jour push APNs et nouveautés Apple Watch.
Le nouveau composant natif WebView de SwiftUI iOS 26 remplace enfin le wrapper UIViewRepresentable autour de WKWebView. Découvrez WebPage, la navigation observable, l'exécution JavaScript en async/await et la migration pratique.