TipKit
为 iOS 17+ 应用添加功能发现提示、上下文提示和引导教练标记,使用 Apple 的 TipKit 框架。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() }
}
}
数据存储位置选项
| 选项 | 使用场景 |
|---|---|
.applicationDefault | 默认位置,应用沙盒(大多数应用) |
.groupContainer(identifier:) | 在应用和扩展之间共享提示状态 |
.url(_:) | 自定义文件 URL,完全控制存储位置 |
CloudKit 同步
在用户的设备之间同步提示状态,避免用户在每个设备上看到相同的提示。将 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 内联显示提示
直接在布局中嵌入 TipView。它会渲染为一个圆角卡片,并以动画方式出现和消失。适用于滚动内容中的提示。
let favoriteTip = FavoriteTip()
var body: some View {
VStack {
TipView(favoriteTip)
ItemListView()
}
}
使用 .popoverTip() 弹出提示
将提示作为弹出窗口附着到任何视图上。框架会从弹出窗口绘制一个指向锚点的箭头。适用于指向特定控件的提示。
Button { toggleFavorite() } label: { Image(systemName: "heart") }
.popoverTip(favoriteTip)
// 控制箭头方向(省略则让系统选择)
.popoverTip(favoriteTip, arrowEdge: .bottom)
自定义 TipViewStyle
创建自定义样式以控制整个应用中的提示外观。遵循 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
使用 ProcessInfo 进行测试方案
if ProcessInfo.processInfo.arguments.contains("--show-all-tips") {
Tips.showAllTipsForTesting()
}
在开发构建的 Xcode 方案中将 --show-all-tips 作为启动参数传递。
常见错误
不要:在 App.init 之外的任何地方调用 Tips.configure()
在视图的 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中调用,在所有视图渲染之前 - 每个提示都有一个清晰简明的标题(以行动为导向,约40个字符以内)
- 当用户执行发现的操作时,提示被失效
- 规则设置合理,使提示在正确的时间出现(并非所有提示都在首次启动时立即出现)
- 当单个视图中存在多个提示时,使用
TipGroup - 测试工具(
showAllTipsForTesting、resetDatastore)受#if DEBUG保护 - 如果应用支持多个设备,配置了 CloudKit 同步
- 适当设置显示频率(大多数应用使用
.daily或.weekly) - 提示仅用于功能发现,不用于关键信息
- 如果默认样式与应用设计不匹配,一致地应用自定义
TipViewStyle - 提示动作已处理,并在动作处理程序中使提示失效
- 事件捐赠放置在正确的用户操作点
- 确保自定义 Tip 类型是 Sendable;在 @MainActor 上配置 Tips
参考
- 参见 references/tipkit-patterns.md 获取完整的实现模式,包括自定义样式、基于事件的规则、提示组、测试策略、引导流程和 SwiftUI 预览配置。