tipkit by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill tipkit使用 Apple 的 TipKit 框架为 iOS 17+ 应用添加功能发现提示、上下文提示和入门引导标记。TipKit 管理显示频率、资格规则和持久性,以便提示在正确的时间出现,并在用户学会功能后消失。
在 App.init 中,在任何视图渲染之前,调用一次 Tips.configure()。这会初始化提示数据存储并开始规则评估。稍后调用可能导致竞争条件,即提示视图尝试在数据存储准备就绪之前显示。
import SwiftUI
import TipKit
@main
struct MyApp: App {
init() {
try? Tips.configure([
.datastoreLocation(.applicationDefault)
])
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
| 选项 | 使用场景 |
|---|---|
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
.applicationDefault| 默认位置,应用沙盒(大多数应用) |
.groupContainer(identifier:) | 在应用和扩展之间共享提示状态 |
.url(_:) | 自定义文件 URL,用于完全控制存储位置 |
跨用户的设备同步提示状态,这样他们就不会在每个设备上都看到相同的提示。在数据存储位置旁边添加 CloudKit 容器选项。
try? Tips.configure([
.datastoreLocation(.applicationDefault),
.cloudKitContainer(.named("iCloud.com.example.app"))
])
使一个结构体遵循 Tip 协议。至少提供一个 title。添加 message 用于支持性细节,添加 image 用于前导图标。保持标题简短且以行动为导向,因为提示以紧凑的标注形式出现。
import TipKit
struct FavoriteTip: Tip {
var title: Text { Text("Pin Your Favorites") }
var message: Text? { Text("Tap the heart icon to save items for quick access.") }
var image: Image? { Image(systemName: "heart") }
}
属性 : title (必需), message (可选细节), image (可选前导图标), actions (可选按钮), rules (可选资格条件), options (显示频率, 最大计数)。
生命周期 : 待定 (规则未满足) -> 符合条件 (所有规则通过) -> 失效 (已关闭、已执行操作或程序化移除)。一旦失效,除非数据存储被重置,否则提示不会再次出现。
直接在布局中嵌入 TipView。它呈现为一个圆角卡片,带有动画显示和消失效果。用于可滚动内容中的提示。
let favoriteTip = FavoriteTip()
var body: some View {
VStack {
TipView(favoriteTip)
ItemListView()
}
}
将提示作为弹出窗口附加到任何视图。框架会从弹出窗口向锚点绘制一个箭头。用于指向特定控件的提示。
Button { toggleFavorite() } label: { Image(systemName: "heart") }
.popoverTip(favoriteTip)
// 控制箭头方向 (省略则让系统选择)
.popoverTip(favoriteTip, arrowEdge: .bottom)
创建自定义样式以控制整个应用中的提示外观。遵循 TipViewStyle 协议并实现 makeBody(configuration:)。
struct CustomTipStyle: TipViewStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(spacing: 12) {
configuration.image?
.font(.title2)
.foregroundStyle(.tint)
VStack(alignment: .leading, spacing: 4) {
configuration.title
.font(.headline)
configuration.message?
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.padding()
}
}
// 全局应用或按视图应用
TipView(favoriteTip)
.tipViewStyle(CustomTipStyle())
规则控制提示何时符合显示条件。在提示显示之前,rules 数组中的所有规则都必须通过。TipKit 支持两种规则类型:基于参数的规则和基于事件的规则。
使用 @Parameter 来跟踪应用状态。当参数值满足规则条件时,提示变为符合条件。
struct FavoriteTip: Tip {
@Parameter
static var hasSeenList: Bool = false
var title: Text { Text("Pin Your Favorites") }
var rules: [Rule] {
#Rule(Self.$hasSeenList) { $0 == true }
}
}
// 当用户到达列表时设置参数
FavoriteTip.hasSeenList = true
使用 Tips.Event 来跟踪用户操作。每次操作发生时,向事件进行捐赠。当捐赠计数或时间条件满足时,规则触发。这非常适合那些应该在用户多次执行某个操作但未发现相关功能后出现的提示。
struct ShortcutTip: Tip {
static let appOpenedEvent = Tips.Event(id: "appOpened")
var title: Text { Text("Try the Quick Action") }
var rules: [Rule] {
#Rule(Self.appOpenedEvent) { $0.donations.count >= 3 }
}
}
// 每次应用打开时捐赠
ShortcutTip.appOpenedEvent.donate()
将多个规则放入数组中。所有规则都必须通过(逻辑与)。
struct AdvancedTip: Tip {
@Parameter
static var isLoggedIn: Bool = false
static let featureUsedEvent = Tips.Event(id: "featureUsed")
var title: Text { Text("Unlock Advanced Mode") }
var rules: [Rule] {
#Rule(Self.$isLoggedIn) { $0 == true }
#Rule(Self.featureUsedEvent) { $0.donations.count >= 5 }
}
}
使用 options 属性控制提示出现的频率。
struct DailyTip: Tip {
var title: Text { Text("Daily Reminder") }
var options: [TipOption] {
MaxDisplayCount(3) // 最多总共显示 3 次
IgnoresDisplayFrequency(true) // 绕过全局频率限制
}
}
全局显示频率在配置时设置:
try? Tips.configure([
.displayFrequency(.daily) // .immediate, .hourly, .daily, .weekly, .monthly
])
使用 .daily 时,系统在整个应用中每天最多显示一个提示,除非特定提示设置了 IgnoresDisplayFrequency(true)。
向提示添加操作按钮以进行直接交互。每个操作都有一个 id 和一个标签。在提示视图的操作处理程序中处理操作。
struct FeatureTip: Tip {
var title: Text { Text("Try the New Editor") }
var message: Text? { Text("We added a powerful new editing mode.") }
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":
navigateToEditor()
featureTip.invalidate(reason: .actionPerformed)
case "learn-more":
showHelpSheet = true
default:
break
}
}
使用 TipGroup 来协调单个视图中的多个提示。TipGroup 确保组中一次只显示一个提示,防止提示过载。提示按优先级顺序显示。
struct OnboardingView: View {
let tipGroup = TipGroup(.ordered) {
WelcomeTip()
NavigationTip()
ProfileTip()
}
var body: some View {
VStack {
if let currentTip = tipGroup.currentTip {
TipView(currentTip)
}
Button("Next") {
tipGroup.currentTip?.invalidate(reason: .actionPerformed)
}
}
}
}
| 初始化器 | 行为 |
|---|---|
.ordered | 提示按列出的顺序显示 |
当当前提示失效时,组中下一个符合条件的提示成为 currentTip。
当用户执行了发现的操作或提示不再相关时,调用 invalidate(reason:)。
let tip = FavoriteTip()
tip.invalidate(reason: .actionPerformed)
| 原因 | 何时使用 |
|---|---|
.actionPerformed | 用户执行了提示描述的操作 |
.displayCountExceeded | 提示达到了其最大显示次数 |
.tipClosed | 用户明确关闭了提示 |
TipKit 提供了静态方法,用于在开发和测试期间控制提示可见性。将这些方法放在 #if DEBUG 或 ProcessInfo 检查后面,以确保它们永远不会在生产构建中运行。
#if DEBUG
// 显示所有提示,无论规则如何(在开发期间有用)
Tips.showAllTipsForTesting()
// 仅显示特定提示
Tips.showTipsForTesting([FavoriteTip.self, ShortcutTip.self])
// 隐藏所有提示(对于不涉及提示的 UI 测试有用)
Tips.hideAllTipsForTesting()
// 重置数据存储(清除所有提示状态、失效和事件)
try? Tips.resetDatastore()
#endif
if ProcessInfo.processInfo.arguments.contains("--show-all-tips") {
Tips.showAllTipsForTesting()
}
在 Xcode 方案中为开发构建传递 --show-all-tips 作为启动参数。
在视图的 onAppear 或 task 修饰符中调用 Tips.configure() 会创建竞争条件,导致提示视图尝试在数据存储准备就绪之前渲染,从而造成提示缺失或闪烁。
// 错误
struct ContentView: View {
var body: some View {
Text("Hello")
.task { try? Tips.configure() } // 太晚了,视图已经渲染
}
}
// 正确
@main struct MyApp: App {
init() { try? Tips.configure() }
var body: some Scene { WindowGroup { ContentView() } }
}
同时显示多个提示会让用户不知所措,并削弱每个提示的效果。用户会学会忽略它们。
// 错误:同时显示三个提示
VStack {
TipView(tipA)
TipView(tipB)
TipView(tipC)
}
// 正确:使用 TipGroup 对它们进行排序
let group = TipGroup(.ordered) { TipA(); TipB(); TipC() }
if let currentTip = group.currentTip {
TipView(currentTip)
}
如果一个提示说“点击星标以收藏”,而用户点击了星标但提示仍然存在,这会削弱用户对 UI 的信任。
// 错误:用户操作后提示仍然可见
Button("Favorite") { toggleFavorite() }
.popoverTip(favoriteTip)
// 正确:在操作时使提示失效
Button("Favorite") {
toggleFavorite()
favoriteTip.invalidate(reason: .actionPerformed)
}
.popoverTip(favoriteTip)
Tips.showAllTipsForTesting() 会绕过所有规则和频率限制。在生产环境中发布这意味着每个用户都会立即看到每个提示。
// 错误:始终激活
Tips.showAllTipsForTesting()
// 正确:放在 DEBUG 后面
#if DEBUG
Tips.showAllTipsForTesting()
#endif
长标题会在紧凑的提示标注中被截断或换行不当。将关键操作放在标题中,将支持性上下文放在消息中。
// 错误
var title: Text { Text("You can tap the heart button to save this item to your favorites list") }
// 正确
var title: Text { Text("Save to Favorites") }
var message: Text? { Text("Tap the heart icon to keep items for quick access.") }
用户可以随时关闭提示,并且提示不会再次出现。切勿将基本说明或安全信息放在提示中。
// 错误:关键信息放在可关闭的提示中
struct DataLossTip: Tip {
var title: Text { Text("Unsaved changes will be lost") }
}
// 正确:对于关键信息,使用警告或内联警告
// 将提示保留用于功能发现和渐进式披露
Tips.configure() 在 App.init 中调用,在任何视图渲染之前TipGroupshowAllTipsForTesting, resetDatastore) 放在 #if DEBUG 后面.daily 或 .weekly)TipViewStylereferences/tipkit-patterns.md 以获取完整的实现模式,包括自定义样式、基于事件的规则、提示组、测试策略、入门流程和 SwiftUI 预览配置。每周安装量
389
代码仓库
GitHub 星标数
276
首次出现
Mar 3, 2026
安全审计
安装于
codex386
cursor383
amp383
cline383
github-copilot383
kimi-cli383
Add feature discovery tips, contextual hints, and onboarding coach marks to iOS 17+ apps using Apple's TipKit framework. TipKit manages display frequency, eligibility rules, and persistence so tips appear at the right time and disappear once the user has learned the feature.
Call Tips.configure() once in App.init, before any views render. This initializes the tips datastore and begins rule evaluation. Calling it later risks a race where tip views attempt to display before the datastore is ready.
import SwiftUI
import TipKit
@main
struct MyApp: App {
init() {
try? Tips.configure([
.datastoreLocation(.applicationDefault)
])
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
| Option | Use Case |
|---|---|
.applicationDefault | Default location, app sandbox (most apps) |
.groupContainer(identifier:) | Share tips state across app and extensions |
.url(_:) | Custom file URL for full control over storage location |
Sync tip state across a user's devices so they do not see the same tip on every device. Add the CloudKit container option alongside the datastore location.
try? Tips.configure([
.datastoreLocation(.applicationDefault),
.cloudKitContainer(.named("iCloud.com.example.app"))
])
Conform a struct to the Tip protocol. Provide a title at minimum. Add message for supporting detail and image for a leading icon. Keep titles short and action-oriented because the tip appears as a compact callout.
import TipKit
struct FavoriteTip: Tip {
var title: Text { Text("Pin Your Favorites") }
var message: Text? { Text("Tap the heart icon to save items for quick access.") }
var image: Image? { Image(systemName: "heart") }
}
Properties : title (required), message (optional detail), image (optional leading icon), actions (optional buttons), rules (optional eligibility conditions), options (display frequency, max count).
Lifecycle : Pending (rules unsatisfied) -> Eligible (all rules pass) -> Invalidated (dismissed, actioned, or programmatically removed). Once invalidated, a tip does not reappear unless the datastore is reset.
Embed a TipView directly in your layout. It renders as a rounded card that appears and disappears with animation. Use for tips within scrollable content.
let favoriteTip = FavoriteTip()
var body: some View {
VStack {
TipView(favoriteTip)
ItemListView()
}
}
Attach a tip as a popover anchored to any view. The framework draws an arrow from the popover to the anchor. Use for tips pointing to a specific control.
Button { toggleFavorite() } label: { Image(systemName: "heart") }
.popoverTip(favoriteTip)
// Control arrow direction (omit to let system choose)
.popoverTip(favoriteTip, arrowEdge: .bottom)
Create a custom style to control tip appearance across the app. Conform to TipViewStyle and implement makeBody(configuration:).
struct CustomTipStyle: TipViewStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(spacing: 12) {
configuration.image?
.font(.title2)
.foregroundStyle(.tint)
VStack(alignment: .leading, spacing: 4) {
configuration.title
.font(.headline)
configuration.message?
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.padding()
}
}
// Apply globally or per view
TipView(favoriteTip)
.tipViewStyle(CustomTipStyle())
Rules control when a tip becomes eligible. All rules in the rules array must pass before the tip displays. TipKit supports two rule types: parameter-based and event-based.
Use @Parameter to track app state. The tip becomes eligible when the parameter value satisfies the rule condition.
struct FavoriteTip: Tip {
@Parameter
static var hasSeenList: Bool = false
var title: Text { Text("Pin Your Favorites") }
var rules: [Rule] {
#Rule(Self.$hasSeenList) { $0 == true }
}
}
// Set the parameter when the user reaches the list
FavoriteTip.hasSeenList = true
Use Tips.Event to track user actions. Donate to the event each time the action occurs. The rule fires when the donation count or timing condition is met. This is ideal for tips that should appear after the user has performed an action several times without discovering a related feature.
struct ShortcutTip: Tip {
static let appOpenedEvent = Tips.Event(id: "appOpened")
var title: Text { Text("Try the Quick Action") }
var rules: [Rule] {
#Rule(Self.appOpenedEvent) { $0.donations.count >= 3 }
}
}
// Donate each time the app opens
ShortcutTip.appOpenedEvent.donate()
Place multiple rules in the array. All must pass (logical AND).
struct AdvancedTip: Tip {
@Parameter
static var isLoggedIn: Bool = false
static let featureUsedEvent = Tips.Event(id: "featureUsed")
var title: Text { Text("Unlock Advanced Mode") }
var rules: [Rule] {
#Rule(Self.$isLoggedIn) { $0 == true }
#Rule(Self.featureUsedEvent) { $0.donations.count >= 5 }
}
}
Control how often tips appear using the options property.
struct DailyTip: Tip {
var title: Text { Text("Daily Reminder") }
var options: [TipOption] {
MaxDisplayCount(3) // Show at most 3 times total
IgnoresDisplayFrequency(true) // Bypass global frequency limit
}
}
Global display frequency is set at configuration time:
try? Tips.configure([
.displayFrequency(.daily) // .immediate, .hourly, .daily, .weekly, .monthly
])
With .daily, the system shows at most one tip per day across the entire app, unless a specific tip sets IgnoresDisplayFrequency(true).
Add action buttons to a tip for direct interaction. Each action has an id and a label. Handle the action in the tip view's action handler.
struct FeatureTip: Tip {
var title: Text { Text("Try the New Editor") }
var message: Text? { Text("We added a powerful new editing mode.") }
var actions: [Action] {
Action(id: "open-editor", title: "Open Editor")
Action(id: "learn-more", title: "Learn More")
}
}
Handle actions in the view:
TipView(featureTip) { action in
switch action.id {
case "open-editor":
navigateToEditor()
featureTip.invalidate(reason: .actionPerformed)
case "learn-more":
showHelpSheet = true
default:
break
}
}
Use TipGroup to coordinate multiple tips within a single view. TipGroup ensures only one tip from the group displays at a time, preventing tip overload. Tips display in priority order.
struct OnboardingView: View {
let tipGroup = TipGroup(.ordered) {
WelcomeTip()
NavigationTip()
ProfileTip()
}
var body: some View {
VStack {
if let currentTip = tipGroup.currentTip {
TipView(currentTip)
}
Button("Next") {
tipGroup.currentTip?.invalidate(reason: .actionPerformed)
}
}
}
}
| Initializer | Behavior |
|---|---|
.ordered | Tips display in the order they are listed |
When the current tip is invalidated, the next eligible tip in the group becomes currentTip.
Call invalidate(reason:) when the user performs the discovered action or when the tip is no longer relevant.
let tip = FavoriteTip()
tip.invalidate(reason: .actionPerformed)
| Reason | When to Use |
|---|---|
.actionPerformed | User performed the action the tip describes |
.displayCountExceeded | Tip hit its maximum display count |
.tipClosed | User explicitly dismissed the tip |
TipKit provides static methods to control tip visibility during development and testing. Gate these behind #if DEBUG or ProcessInfo checks so they never run in production builds.
#if DEBUG
// Show all tips regardless of rules (useful during development)
Tips.showAllTipsForTesting()
// Show only specific tips
Tips.showTipsForTesting([FavoriteTip.self, ShortcutTip.self])
// Hide all tips (useful for UI tests that do not involve tips)
Tips.hideAllTipsForTesting()
// Reset the datastore (clears all tip state, invalidations, and events)
try? Tips.resetDatastore()
#endif
if ProcessInfo.processInfo.arguments.contains("--show-all-tips") {
Tips.showAllTipsForTesting()
}
Pass --show-all-tips as a launch argument in the Xcode scheme for development builds.
Calling Tips.configure() in a view's onAppear or task modifier creates a race condition where tip views try to render before the datastore is ready, causing missing or flickering tips.
// WRONG
struct ContentView: View {
var body: some View {
Text("Hello")
.task { try? Tips.configure() } // Too late, views already rendered
}
}
// CORRECT
@main struct MyApp: App {
init() { try? Tips.configure() }
var body: some Scene { WindowGroup { ContentView() } }
}
Displaying multiple tips simultaneously overwhelms users and dilutes the impact of each tip. Users learn to ignore them.
// WRONG: Three tips visible at the same time
VStack {
TipView(tipA)
TipView(tipB)
TipView(tipC)
}
// CORRECT: Use TipGroup to sequence them
let group = TipGroup(.ordered) { TipA(); TipB(); TipC() }
if let currentTip = group.currentTip {
TipView(currentTip)
}
If a tip says "Tap the star to favorite" and the user taps the star but the tip remains, it erodes trust in the UI.
// WRONG: Tip stays visible after user acts
Button("Favorite") { toggleFavorite() }
.popoverTip(favoriteTip)
// CORRECT: Invalidate on action
Button("Favorite") {
toggleFavorite()
favoriteTip.invalidate(reason: .actionPerformed)
}
.popoverTip(favoriteTip)
Tips.showAllTipsForTesting() bypasses all rules and frequency limits. Shipping this in production means every user sees every tip immediately.
// WRONG: Always active
Tips.showAllTipsForTesting()
// CORRECT: Gated behind DEBUG
#if DEBUG
Tips.showAllTipsForTesting()
#endif
Long titles get truncated or wrap awkwardly in the compact tip callout. Put the key action in the title and supporting context in the message.
// WRONG
var title: Text { Text("You can tap the heart button to save this item to your favorites list") }
// CORRECT
var title: Text { Text("Save to Favorites") }
var message: Text? { Text("Tap the heart icon to keep items for quick access.") }
Users can dismiss tips at any time and they do not reappear. Never put essential instructions or safety information in a tip.
// WRONG: Critical info in a dismissible tip
struct DataLossTip: Tip {
var title: Text { Text("Unsaved changes will be lost") }
}
// CORRECT: Use an alert or inline warning for critical information
// Reserve tips for feature discovery and progressive disclosure
Tips.configure() called in App.init, before any views renderTipGroup used when multiple tips exist in one viewshowAllTipsForTesting, resetDatastore) gated behind #if DEBUG.daily or .weekly for most apps)TipViewStyle applied consistently if the default style does not match the app designreferences/tipkit-patterns.md for complete implementation patterns including custom styles, event-based rules, tip groups, testing strategies, onboarding flows, and SwiftUI preview configuration.Weekly Installs
389
Repository
GitHub Stars
276
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex386
cursor383
amp383
cline383
github-copilot383
kimi-cli383
Refero Design:研究优先设计方法,学习最佳实践,打造独特用户体验
838 周安装
竞争对手研究指南:SEO、内容、反向链接与定价分析工具
231 周安装
Azure 工作负载自动升级评估工具 - 支持 Functions、App Service 计划与 SKU 迁移
231 周安装
Kaizen持续改进方法论:软件开发中的渐进式优化与防错设计实践指南
231 周安装
软件UI/UX设计指南:以用户为中心的设计原则、WCAG可访问性与平台规范
231 周安装
Apify 网络爬虫和自动化平台 - 无需编码抓取亚马逊、谷歌、领英等网站数据
231 周安装
llama.cpp 中文指南:纯 C/C++ LLM 推理,CPU/非 NVIDIA 硬件优化部署
231 周安装