实现并审查 Apple TipKit 功能发现 UI,适用于 iOS 17+ 应用。在添加或审核应用内提示、上下文帮助、教练标记、Tip、TipView、popoverTip、规则、事件、操作、显示频率、测试覆盖、可重用提示标识符或 iOS 18+ 的 TipGroup 和 CloudKit 提示同步时使用;避免用于通用 SwiftUI 导航或提示展示之外的布局。
TipKit
使用 TipKit 实现小型、上下文相关的功能发现时刻:内联提示、弹出提示、规则控制的教育和轻量级教练标记。将通用的 SwiftUI 架构、导航、布局和较长的首次运行引导流程保留在兄弟技能中,除非 TipKit 展示是核心问题。
目录
可用性
TipKit 的核心 Tip、TipView、popoverTip、规则、事件、选项和测试覆盖在 iOS 17+、iPadOS 17+、macOS 14+、tvOS 17+、watchOS 10+ 和 visionOS 1+ 上可用。
明确地门控较新的 API:
| API | 可用性 | 用途 |
|---|---|---|
TipGroup |
iOS 18+ | 分组或排序提示;应用提示组决策。 |
.cloudKitContainer(...) |
iOS 18+ | 跨设备同步提示状态、参数、事件和显示次数。 |
MaxDisplayDuration |
iOS 18+ | 在累计显示时间后自动失效。 |
resetEligibility() |
iOS 26+ | 使之前失效的提示重新符合条件,无需重置数据存储。 |
配置 TipKit
在应用初始化期间调用 Tips.configure(_:) 一次,在任何提示可以显示之前。不要从视图的 onAppear 或 .task 中配置 TipKit。
import SwiftUI
import TipKit
@main
struct MyApp: App {
init() {
do {
try Tips.configure([
.datastoreLocation(.applicationDefault),
.displayFrequency(.daily)
])
} catch {
assertionFailure("TipKit 配置失败: \(error)")
}
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
仅当应用和扩展或应用组成员有意共享提示状态时,使用 .datastoreLocation(.groupContainer(identifier:))。保持应用组成员的选项设置一致,因为 TipKit 会将选项状态与提示记录一起持久化。
CloudKit 同步
仅在 iOS 18+ 及更高版本上使用 CloudKit 同步。启用 iCloud + CloudKit 和后台模式 > 远程通知,然后传递容器:
try Tips.configure([
.cloudKitContainer(.named("iCloud.com.example.app.tips"))
])
优先使用带有 .tips 后缀的专用容器。.automatic 在存在时使用第一个有权限的 .tips 容器,否则回退到主容器。
设计好的提示
提示是小型、临时的帮助。将它们用于用户可以在几个简单步骤中理解和尝试的功能。如果流程需要长篇解释、多个屏幕或关键的安全/错误信息,请改用教程、警报、内联警告或引导流程。
遵循 HIG 对齐的默认值:
- 保持标题简短、直接且面向操作。
- 使用一到两句话;避免促销或不相关的文案。
- 将提示放置在所解释功能附近。
- 当隐藏附近 UI 会中断任务时,优先使用内联提示。
- 当保留当前布局很重要且提示可以指向特定控件时,优先使用弹出提示。
- 使用规则和显示频率,以便只有正确的受众看到每个提示。
- 当弹出提示已经指向该图标时,避免在提示中重复图标。
定义提示
Tip 遵循 Identifiable 和 Sendable。至少提供 title;仅在它们改善功能发现时刻时添加 message、image、actions、rules、options 和 id。
import TipKit
struct FavoriteTip: Tip {
var title: Text { Text("保存到收藏夹") }
var message: Text? { Text("点击心形图标以保留项目以便快速访问。") }
var image: Image? { Image(systemName: "heart.fill") }
}
默认情况下,TipKit 使用提示类型名称作为 id。为可重用提示覆盖 id,其持久化状态应根据内容变化:
struct NewItemTip: Tip {
let itemID: Item.ID
var id: String { "NewItemTip-\(itemID)" }
var title: Text { Text("新项目可用") }
}
使用稳定、具体的标识符。不要从临时文案或不稳定的排序中派生 ID。
展示提示
使用 TipView 进行内联提示:
let favoriteTip = FavoriteTip()
VStack {
TipView(favoriteTip, arrowEdge: .bottom)
ItemListView()
}
当提示应指向控件时,使用 .popoverTip:
Button {
toggleFavorite()
favoriteTip.invalidate(reason: .actionPerformed)
} label: {
Image(systemName: "heart")
}
.popoverTip(favoriteTip, arrowEdge: .top)
规则和事件
规则是 AND 连接的。只有当每个规则都通过时,提示才符合条件。
使用 @Parameter 持久化应用状态:
struct FavoriteTip: Tip {
@Parameter static var hasSeenList = false
var title: Text { Text("保存到收藏夹") }
var rules: [Rule] {
#Rule(Self.$hasSeenList) { $0 == true }
}
}
使用 Tips.Event 处理重复的用户操作。TipKit 默认查询最近 1000 次捐赠,因此保持事件规则有界且有意。
struct ShortcutTip: Tip {
static let manualSaveEvent = Tips.Event(id: "manualSave")
var title: Text { Text("更快保存") }
var rules: [Rule] {
#Rule(Self.manualSaveEvent) {
$0.donations.donatedWithin(.week).count >= 3
}
}
}
ShortcutTip.manualSaveEvent.sendDonation()
对于更丰富的事件规则,定义 Tips.Event<DonationInfo>,其中 DonationInfo: Codable, Sendable。保持捐赠负载较小。
当多个提示使用相同事件时,在共享命名空间中分组相关事件定义;事件 ID 是持久化边界,因此冲突可能导致令人困惑的资格条件。
选项和失效
谨慎使用选项;频率和失效规则是提示持久化行为的一部分。
struct DailyTip: Tip {
var title: Text { Text("尝试过滤器") }
var options: [any TipOption] {
MaxDisplayCount(3)
IgnoresDisplayFrequency(false)
}
}
MaxDisplayDuration 是 iOS 18+ 的。它计算累计显示时间,并在自动失效发生前有一个最小连续显示持续时间。当应用知道教授的操作或有序步骤已完成时,不要将其用作显式 invalidate(reason:) 的替代品。
当用户执行发现的操作或提示不再相关时,调用 invalidate(reason:)。失效是永久性的,直到数据存储被重置,或者在 iOS 26+ 上,特定提示调用 await resetEligibility()。
favoriteTip.invalidate(reason: .actionPerformed)
使用 .tipClosed 表示显式关闭,仅当描述自动失效结果时使用 .displayCountExceeded 或 .displayDurationExceeded。
操作和样式
当用户需要直接路径到设置、更多信息或设置流程时,添加 Action 按钮。
struct FeatureTip: Tip {
var title: Text { Text("尝试新编辑器") }
var actions: [Action] {
Action(id: "open-editor", title: "打开编辑器")
Action(id: "learn-more", title: "了解更多")
}
}
TipView(FeatureTip()) { action in
switch action.id {
case "open-editor":
openEditor()
case "learn-more":
showHelp()
default:
break
}
}
对于自定义外观,优先使用 TipViewStyle.Configuration 值,而不是直接从具体提示实例读取。这样可以保留应用于 TipView 的标签、处理程序和修饰符。
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()
}
}
提示组
TipGroup 是 iOS 18+ 的。将组存储在 SwiftUI 状态中,以便可观察的组对象在视图更新中持久存在。在每次审查 TipGroup(.ordered) 计划时,明确区分默认优先级和有序序列:TipGroup 默认为 .firstAvailable,当每个后续提示必须等待所有先前提示失效时,需要 TipGroup(.ordered)。
struct OnboardingView: View {
@State private var tips = TipGroup(.ordered) {
WelcomeTip()
SearchTip()
FilterTip()
}
var body: some View {
VStack {
TipView(tips.currentTip)
ContentView()
}
}
}
MaxDisplayDuration 可以限制显示时间,但它不是有序组的排序机制。当同一组跨越多个控件时,转换 currentTip:
Button("搜索") { openSearch() }
.popoverTip(tips.currentTip as? SearchTip)
测试
仅在调试/测试代码中使用测试覆盖,并在 Tips.configure(_:) 之前应用它们。
#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()
内置启动参数也可用:
-com.apple.TipKit.ResetDatastore 1-com.apple.TipKit.ShowAllTips 1-com.apple.TipKit.ShowTips TipTypeA,TipTypeB-com.apple.TipKit.HideAllTips 1
测试覆盖优先级为:特定显示、特定隐藏、全部显示、然后全部隐藏。Tips.resetDatastore() 必须在 Tips.configure(_:) 之前运行。
常见错误
不要:从视图中配置 TipKit
在应用初始化期间配置。视图级别的配置可能与提示显示竞争,并且可能遇到数据存储已配置的错误。
不要:将 iOS 18+ API 作为 iOS 17 指导呈现
门控 TipGroup、CloudKit 同步和 MaxDisplayDuration。对于组优先级,应用规范的提示组决策。
不要:将提示用于关键信息
提示是可关闭的和教育性的。对于安全、错误、数据丢失和必需步骤,使用警报、确认、内联警告或阻塞 UI。
不要:发布测试覆盖
showAllTipsForTesting() 和相关覆盖绕过规则和频率限制。将它们保留在 #if DEBUG、测试方案参数或仅 UI 测试的启动参数后面。
不要:使用不稳定的可重用提示 ID
提示 ID 拥有持久化。如果可重用提示的 ID 意外更改,用户可能会看到重复或过时的教育内容。
审查清单
- [ ]
Tips.configure(_:)在应用初始化期间运行一次,在提示显示之前。 - [ ]
Tips.resetDatastore()仅在配置之前运行,且仅用于测试/调试。 - [ ] iOS 18+ 和 iOS 26+ 的 TipKit API 具有可用性门控或回退指导。
- [ ] 提示文案简短、上下文相关、可操作且非促销。
- [ ] 内联与弹出展示与周围 UI 流程匹配。
- [ ] 规则针对目标受众,并且不在首次启动时显示每个提示。
- [ ] 事件 ID 稳定,共享时命名空间化,捐赠负载较小。
- [ ] 可重用提示使用稳定的内容派生值覆盖
id。 - [ ] 当用户执行教授的操作时,提示失效。
- [ ]
TipGroup保持在@State中,并遵循提示组优先级决策。 - [ ] CloudKit 同步使用 iCloud + CloudKit、远程通知,并在适当时使用专用容器。
- [ ] 自定义样式使用
configuration值并调用action.label()。 - [ ] 测试覆盖仅用于调试/测试,并且永远不会在生产中激活。
参考
- 阅读 references/tipkit-patterns.md 获取完整的实现模式:自定义样式、带捐赠值的事件规则、TipGroup 排序、CloudKit/应用组持久化、可重用 ID、预览和测试启动策略。
- Apple TipKit 文档: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 "提供帮助":https://sosumi.ai/design/human-interface-guidelines/offering-help
- WWDC24 "使用 TipKit 自定义功能发现":https://sosumi.ai/videos/play/wwdc2024/10070
- WWDC23 "使用 TipKit 使功能可发现":https://sosumi.ai/videos/play/wwdc2023/10229






