rshankras/tipkit-generator
Generate TipKit infrastructure with inline/popover tips, rules, display frequency, and testing utilities. Use when adding contextual tips or feature discovery to an iOS/macOS app.
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill tipkit-generator
Generate a complete TipKit setup for contextual tips and feature discovery, including tip definitions, rules, display frequency, inline and popover presentation, and testing utilities.
Use this skill when the user:
Search for existing TipKit usage:
Glob: **/*Tip*.swift
Grep: "import TipKit" or "Tips.configure"
If found, ask user:
Ask user via AskUserQuestion:
Read the templates file for code patterns:
Read("skills/generators/tipkit-generator/templates.md")
Generate these files based on configuration:
Tips/ directory with one file per tip (e.g., SearchTip.swift, FilterTip.swift)Tips/TipEvents.swift - Centralized event definitionsTips/TipsConfiguration.swift - Tips.configure() setup and testing utilitiesCheck project structure:
Sources/ exists -> Sources/Tips/App/ exists -> App/Tips/Tips/Tips.configure() call in App entry pointTipView or .popoverTip() at the appropriate view locationsAfter generation, provide:
Sources/Tips/
├── SearchTip.swift # Tip with rules and options
├── FilterTip.swift # Another tip definition
├── TipEvents.swift # Centralized event definitions
└── TipsConfiguration.swift # Tips.configure() + testing helpers
App Entry Point (Required):
import TipKit
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
try? Tips.configure([
.displayFrequency(.daily),
.datastoreLocation(.applicationDefault)
])
}
}
}
}
Inline Tip:
import TipKit
struct SearchView: View {
let searchTip = SearchTip()
var body: some View {
VStack {
TipView(searchTip)
SearchBar()
}
}
}
Popover Tip:
import TipKit
struct ToolbarView: View {
let filterTip = FilterTip()
var body: some View {
Button("Filter", systemImage: "line.3.horizontal.decrease.circle") {
// action
}
.popoverTip(filterTip)
}
}
Event Donation (at action site):
Button("Search") {
performSearch()
SearchTip.searchPerformed.donate()
}
Tip Invalidation (when tip is no longer relevant):
func onFeatureUsed() {
// User discovered the feature, invalidate the tip
searchTip.invalidate(reason: .actionPerformed)
}
// Add to a debug menu or call in preview
try? Tips.resetDatastore()
// Ignores rules and frequency -- shows everything
Tips.showAllTipsForTesting()
Tips.showTipsForTesting([SearchTip.self])
Tips.configure() before any tip is displayed. This must happen early, typically in the App body or .task.@Parameter static var hasSeenFeature = false but never set it to true, the rule never passes..datastoreLocation(.url(...)) to isolate them..displayFrequency(.daily) and a tip was already shown today, no new tips will appear until tomorrow. Use .immediate during development..invalidate(reason:) on that instance. Creating a new instance and invalidating it does nothing to the displayed tip.Good tips are actionable, instructional, and easy to remember:
Never use tips for:
$0.donations.donatedWithin(.days(5)).count >= 3 — "3 times in the past 5 days", not just a lifetime countIgnoresDisplayFrequency(true) tip option exempts an urgent tip from the global .displayFrequency cadenceMaxDisplayCount(n) tip option auto-stops a tip after N impressions, even if it is never dismissed or invalidated.actionPerformed invalidation reason when the user completes the action the tip describesTipGroup when tips should appear in a logical sequenceTips.resetDatastore()Tips.configure() in the App entry point.immediate display frequency in production (overwhelming users)TipView inside a ScrollView without considering layout impactTake rshankras/tipkit-generator from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.