tipkit
Implement and review Apple TipKit feature-discovery UI for iOS 17+ apps. Use when adding or auditing in-app tips, contextual help, coach marks, Tip, TipView, popoverTip, rules, events, actions, display frequency, testing overrides, reusable tip identifiers, or iOS 18+ TipGroup and CloudKit tip sync; avoid for generic SwiftUI navigation or layout outside tip presentation.
Other options
Summary
Implement and review Apple TipKit feature-discovery UI for iOS 17+ apps. Use when adding or auditing in-app tips, contextual help, coach marks, Tip, TipView, popoverTip, rules, events, actions, display frequency, testing overrides, reusable tip identifiers, or iOS 18+ TipGroup and CloudKit tip sync; avoid for generic SwiftUI navigation or layout outside tip presentation.
Raw SKILL.md
13.2K bytes---
name: tipkit
description: "Implement and review Apple TipKit feature-discovery UI for iOS 17+ apps. Use when adding or auditing in-app tips, contextual help, coach marks, Tip, TipView, popoverTip, rules, events, actions, display frequency, testing overrides, reusable tip identifiers, or iOS 18+ TipGroup and CloudKit tip sync; avoid for generic SwiftUI navigation or layout outside tip presentation."
---
# TipKit
Use TipKit for small, contextual feature-discovery moments: inline tips,
popover tips, rule-gated education, and lightweight coach marks. Keep generic
SwiftUI architecture, navigation, layout, and long first-run onboarding flows in
their sibling skills unless TipKit presentation is the core issue.
## Contents
- [Availability](#availability)
- [Configure TipKit](#configure-tipkit)
- [Design Good Tips](#design-good-tips)
- [Define Tips](#define-tips)
- [Present Tips](#present-tips)
- [Rules and Events](#rules-and-events)
- [Options and Invalidation](#options-and-invalidation)
- [Actions and Styles](#actions-and-styles)
- [Tip Groups](#tip-groups)
- [Testing](#testing)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)
## Availability
TipKit's core `Tip`, `TipView`, `popoverTip`, rules, events, options, and
testing overrides are available on iOS 17+, iPadOS 17+, macOS 14+, tvOS 17+,
watchOS 10+, and visionOS 1+.
Gate newer APIs explicitly:
| API | Availability | Use |
| --- | --- | --- |
| `TipGroup` | iOS 18+ | Group or sequence tips; apply the [Tip Groups](#tip-groups) decision. |
| `.cloudKitContainer(...)` | iOS 18+ | Sync tip state, parameters, events, and display counts across devices. |
| `MaxDisplayDuration` | iOS 18+ | Automatically invalidate after cumulative display time. |
| `resetEligibility()` | iOS 26+ | Make a previously invalidated tip eligible again without resetting the datastore. |
## Configure TipKit
Call `Tips.configure(_:)` once during app initialization, before any tip can
display. Do not configure TipKit from a view's `onAppear` or `.task`.
```swift
import SwiftUI
import TipKit
@main
struct MyApp: App {
init() {
do {
try Tips.configure([
.datastoreLocation(.applicationDefault),
.displayFrequency(.daily)
])
} catch {
assertionFailure("TipKit configuration failed: \(error)")
}
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
```
Use `.datastoreLocation(.groupContainer(identifier:))` only when an app and
extension or app-group members intentionally share tip state. Keep option
settings consistent across app-group members because TipKit persists option
state with the tip record.
### CloudKit Sync
Use CloudKit sync only on iOS 18+ and later. Enable iCloud + CloudKit and
Background Modes > Remote notifications, then pass a container:
```swift
try Tips.configure([
.cloudKitContainer(.named("iCloud.com.example.app.tips"))
])
```
Prefer a dedicated container with a `.tips` suffix. `.automatic` uses the first
entitled `.tips` container when present, then falls back to the primary
container.
## Design Good Tips
Tips are small, transient help. Use them for features people can understand and
try in a few simple steps. If the flow needs a long explanation, multiple
screens, or critical safety/error information, use a tutorial, alert, inline
warning, or onboarding flow instead.
Follow HIG-aligned defaults:
- Keep titles short, direct, and action-oriented.
- Use one or two sentences; avoid promotional or unrelated copy.
- Place tips near the feature they explain.
- Prefer inline tips when hiding nearby UI would interrupt the task.
- Prefer popover tips when preserving the current layout matters and the tip can
point to a specific control.
- Use rules and display frequency so only the right audience sees each tip.
- Avoid repeating an icon in the tip when the popover already points to that icon.
## Define Tips
`Tip` conforms to `Identifiable` and `Sendable`. Provide `title` at minimum;
add `message`, `image`, `actions`, `rules`, `options`, and `id` only when they
improve the feature-discovery moment.
```swift
import TipKit
struct FavoriteTip: Tip {
var title: Text { Text("Save to Favorites") }
var message: Text? { Text("Tap the heart to keep items for quick access.") }
var image: Image? { Image(systemName: "heart.fill") }
}
```
By default, TipKit uses the tip type name as `id`. Override `id` for reusable
tips whose persisted state should vary by content:
```swift
struct NewItemTip: Tip {
let itemID: Item.ID
var id: String { "NewItemTip-\(itemID)" }
var title: Text { Text("New Item Available") }
}
```
Use stable, concrete identifiers. Do not derive IDs from transient copy or
unstable ordering.
## Present Tips
Use `TipView` for inline tips:
```swift
let favoriteTip = FavoriteTip()
VStack {
TipView(favoriteTip, arrowEdge: .bottom)
ItemListView()
}
```
Use `.popoverTip` when the tip should point to a control:
```swift
Button {
toggleFavorite()
favoriteTip.invalidate(reason: .actionPerformed)
} label: {
Image(systemName: "heart")
}
.popoverTip(favoriteTip, arrowEdge: .top)
```
## Rules and Events
Rules are ANDed together. A tip becomes eligible only when every rule passes.
Use `@Parameter` for persisted app state:
```swift
struct FavoriteTip: Tip {
@Parameter static var hasSeenList = false
var title: Text { Text("Save to Favorites") }
var rules: [Rule] {
#Rule(Self.$hasSeenList) { $0 == true }
}
}
```
Use `Tips.Event` for repeated user actions. TipKit queries the most recent 1000
donations by default, so keep event rules bounded and intentional.
```swift
struct ShortcutTip: Tip {
static let manualSaveEvent = Tips.Event(id: "manualSave")
var title: Text { Text("Save Faster") }
var rules: [Rule] {
#Rule(Self.manualSaveEvent) {
$0.donations.donatedWithin(.week).count >= 3
}
}
}
ShortcutTip.manualSaveEvent.sendDonation()
```
For richer event rules, define `Tips.Event<DonationInfo>` where
`DonationInfo: Codable, Sendable`. Keep donation payloads small.
Group related event definitions in a shared namespace when several tips use the
same events; event IDs are the persistence boundary, so collisions can create
confusing eligibility.
## Options and Invalidation
Use options sparingly; frequency and invalidation rules are part of the tip's
persisted behavior.
```swift
struct DailyTip: Tip {
var title: Text { Text("Try Filters") }
var options: [any TipOption] {
MaxDisplayCount(3)
IgnoresDisplayFrequency(false)
}
}
```
`MaxDisplayDuration` is iOS 18+. It counts cumulative display time and has a
minimum continuous display duration before automatic invalidation can occur.
Do not use it as a replacement for explicit `invalidate(reason:)` when the app
knows the taught action or ordered step is complete.
Call `invalidate(reason:)` when the user performs the discovered action or the
tip is no longer relevant. Invalidation is permanent until the datastore is
reset or, on iOS 26+, the specific tip calls `await resetEligibility()`.
```swift
favoriteTip.invalidate(reason: .actionPerformed)
```
Use `.tipClosed` for explicit dismissal and `.displayCountExceeded` or
`.displayDurationExceeded` only when describing automatic invalidation outcomes.
## Actions and Styles
Add `Action` buttons when the user needs a direct route to settings, more
information, or a setup flow.
```swift
struct FeatureTip: Tip {
var title: Text { Text("Try the New Editor") }
var actions: [Action] {
Action(id: "open-editor", title: "Open Editor")
Action(id: "learn-more", title: "Learn More")
}
}
TipView(FeatureTip()) { action in
switch action.id {
case "open-editor":
openEditor()
case "learn-more":
showHelp()
default:
break
}
}
```
For custom appearance, prefer `TipViewStyle.Configuration` values over reading
directly from a concrete tip instance. That preserves labels, handlers, and
modifiers applied to the `TipView`.
```swift
struct CompactTipStyle: TipViewStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .top) {
configuration.image?
VStack(alignment: .leading) {
configuration.title?
configuration.message?
ForEach(configuration.actions) { action in
Button(action: action.handler) {
action.label()
}
}
}
}
.padding()
}
}
```
## Tip Groups
`TipGroup` is iOS 18+. Store groups in SwiftUI state so the observable group
object persists across view updates. In every review of a `TipGroup(.ordered)`
plan, explicitly distinguish the default priority from ordered sequences:
`TipGroup` defaults to `.firstAvailable`, and `TipGroup(.ordered)` is required
when each later tip must wait for all previous tips to be invalidated.
```swift
struct OnboardingView: View {
@State private var tips = TipGroup(.ordered) {
WelcomeTip()
SearchTip()
FilterTip()
}
var body: some View {
VStack {
TipView(tips.currentTip)
ContentView()
}
}
}
```
`MaxDisplayDuration` can cap display time, but it is not the sequencing
mechanism for an ordered group. Cast `currentTip` when the same group spans
multiple controls:
```swift
Button("Search") { openSearch() }
.popoverTip(tips.currentTip as? SearchTip)
```
## Testing
Use testing overrides only in debug/test code, and apply them before
`Tips.configure(_:)`.
```swift
#if DEBUG
if ProcessInfo.processInfo.arguments.contains("--reset-tips") {
try? Tips.resetDatastore()
}
if ProcessInfo.processInfo.arguments.contains("--show-all-tips") {
Tips.showAllTipsForTesting()
}
#endif
try Tips.configure()
```
Built-in launch arguments are also available:
- `-com.apple.TipKit.ResetDatastore 1`
- `-com.apple.TipKit.ShowAllTips 1`
- `-com.apple.TipKit.ShowTips TipTypeA,TipTypeB`
- `-com.apple.TipKit.HideAllTips 1`
Testing override precedence is specific show, specific hide, show all, then hide
all. `Tips.resetDatastore()` must run before `Tips.configure(_:)`.
## Common Mistakes
### DON'T: Configure TipKit from a view
Configure during app initialization. View-level configuration can race with tip
display and can also hit datastore-already-configured errors.
### DON'T: Present iOS 18+ APIs as iOS 17 guidance
Gate `TipGroup`, CloudKit sync, and `MaxDisplayDuration`. For group priority,
apply the canonical [Tip Groups](#tip-groups) decision.
### DON'T: Use tips for critical information
Tips are dismissible and educational. Use alerts, confirmations, inline
warnings, or blocking UI for safety, errors, data loss, and required steps.
### DON'T: Ship testing overrides
`showAllTipsForTesting()` and related overrides bypass rules and frequency
limits. Keep them behind `#if DEBUG`, test scheme arguments, or UI-test-only
launch arguments.
### DON'T: Use unstable reusable tip IDs
Tip IDs own persistence. If a reusable tip's ID changes unexpectedly, users can
see duplicate or stale education.
## Review Checklist
- [ ] `Tips.configure(_:)` runs once during app initialization before tips display.
- [ ] `Tips.resetDatastore()` runs only before configuration and only for tests/debug.
- [ ] iOS 18+ and iOS 26+ TipKit APIs have availability gates or fallback guidance.
- [ ] Tip copy is short, contextual, actionable, and not promotional.
- [ ] Inline vs popover presentation matches the surrounding UI flow.
- [ ] Rules target the intended audience and do not show every tip on first launch.
- [ ] Event IDs are stable, namespaced when shared, and donation payloads are small.
- [ ] Reusable tips override `id` with stable content-derived values.
- [ ] Tips invalidate when the user performs the taught action.
- [ ] `TipGroup` stays in `@State` and follows the Tip Groups priority decision.
- [ ] CloudKit sync uses iCloud + CloudKit, Remote notifications, and a dedicated container when appropriate.
- [ ] Custom styles use `configuration` values and call `action.label()`.
- [ ] Testing overrides are debug/test-only and never ship active in production.
## References
- Read [references/tipkit-patterns.md](references/tipkit-patterns.md) for complete implementation patterns: custom styles, event rules with donation values, TipGroup sequencing, CloudKit/app-group persistence, reusable IDs, previews, and test launch strategies.
- Apple TipKit docs: https://sosumi.ai/documentation/tipkit
- Apple `Tips.configure(_:)`: https://sosumi.ai/documentation/tipkit/tips/configure(_:)
- Apple `TipGroup`: https://sosumi.ai/documentation/tipkit/tipgroup
- Apple HIG "Offering help": https://sosumi.ai/design/human-interface-guidelines/offering-help
- WWDC24 "Customize feature discovery with TipKit": https://sosumi.ai/videos/play/wwdc2024/10070
- WWDC23 "Make features discoverable with TipKit": https://sosumi.ai/videos/play/wwdc2023/10229

