Customize feature discovery with TipKit
System Frameworks 进阶 20m

用 TipKit 打造更灵活的功能引导体验

Customize feature discovery with TipKit

2024年6月10日

在 Apple 官方观看视频

一句话判断

TipKit 今年补齐了功能引导最缺的三块拼图:分组控制展示顺序、自定义标识符让 Tip 可复用、CloudKit 同步让跨设备状态一致——从此功能引导不再是一锤子买卖。

这场 Session 讲了什么

TipKit 在 iOS 17 发布时解决了”怎么展示 Tip”的问题,但实际用下来你会发现:多个 Tip 可能同时弹出、同一个 Tip 结构没法复用给不同内容、换台设备 Tip 状态又重置了。今年的更新直击这些痛点。

Session 围绕四个能力展开:TipGroup 控制多个 Tip 的展示顺序和互斥逻辑;自定义 Identifier 让一个 Tip 结构体可以绑定不同数据源反复使用;TipViewStyle 把 Tip 的视觉表现彻底交给开发者定制;CloudKit 同步则让 Tip 的展示状态(已读、已失效)在所有设备上保持一致。

这些能力叠加在一起,意味着你可以构建一套完整的功能引导策略——从首次安装到版本更新,从 iPhone 到 iPad,TipKit 都能管起来。

值得深挖的点

TipGroup:给 Tip 排队,而不是让它们打架

之前 TipKit 最大的问题是多个 Tip 同时出现。你在地图上加了一个指南针控件,想分别引导”点击定位”和”长按旋转”两个操作,结果两个 Popover 同时冒出来,用户一脸懵。

TipGroup 的解法很直接:你把一组 Tip 放进同一个 Group 里,指定 .ordered 优先级,第一个 Tip 不失效(用户没看过、没手动关闭、也没触发对应操作),第二个就永远不出来。这比自己在业务层维护状态队列要干净得多。

另一个优先级 .firstAvailable 适合无关联的 Tip 场景——同一屏有多个不相干的 Tip,但只想同时展示一个。配合 displayFrequency 使用,可以把 Tip 的节奏控制得很舒服,不会在首次启动时就用信息轰炸用户。

自定义 Identifier:一个 Tip 结构体,多种数据源

这个设计解决了一个真实的扩展性问题。假设你有一个”新路线提醒”的 Tip,每当你 App 里新增一条徒步路线就要加一个新 Tip 结构体——代码膨胀、多个 TipView 可能同时出现。

自定义 ID 的思路是:Tip 的 identifier 不再用默认的类型名,而是绑定具体的数据(比如路线名称)。这样一来,同一个 NewTrailTip 结构体实例化多次,每次传入不同的路线对象,每个实例都有独立的状态和规则。用户在 A 路线上关闭了 Tip,不影响 B 路线的 Tip 展示。

关键点在于 Identifier 要绑定”稳定的、具体的值”(用户 ID、内容 ID),而不是动态变化的属性。因为 TipKit 会为每个 Identifier 创建持久化记录,用来追踪跨多次启动的事件规则。

代码片段

TipGroup 控制展示顺序

// 两个相关的功能引导,按顺序展示
struct ShowLocationTip: Tip { /* 点击指南针定位 */ }
struct RotateMapTip: Tip { /* 长按指南针旋转 */ }

// ordered 确保第一个 Tip 失效后第二个才会出现
let compassTips = TipGroup(ordered: [ShowLocationTip(), RotateMapTip()])

// 在视图上使用 currentTip 属性
.compassView
    .popoverTip(compassTips.currentTip)

场景:地图指南针有两个功能需要引导。用 ordered Group 确保用户先学定位再学长按旋转,避免信息过载。坑:别忘了在用户实际触发对应操作时调用 tip.invalidate(reason: .actionPerformed),否则后面的 Tip 永远不会出现。

自定义 Identifier 实现可复用 Tip

struct NewTrailTip: Tip {
    let trail: Trail
    // 用路线名称作为 ID,每个路线有独立状态
    var id: String { trail.name }

    var title: Text { Text("发现新路线") }
    var message: Text { Text("\(trail.name) 位于 \(trail.region)") }

    // 只对该路线所在区域的用户展示
    @Parameter var didVisit: Bool = false
    var rules: [Rule] {
        Rule(#Predicate { $0.didVisit })
    }
}

场景:App 动态新增内容时,用同一个 Tip 结构体为每条新内容创建独立引导。坑:不要用 UUID() 做 ID——每次运行都会生成新值,之前的状态记录就全丢了。

CloudKit 同步 Tip 状态

// 在 App 启动时配置 CloudKit 同步
try await Tips.configure([
    .cloudKitSyncEnabled(true),
    .displayFrequency(.weekly)
])

场景:用户在 iPhone 上已经看过某个 Tip,换到 iPad 上不该再弹一次。一行配置开启同步。坑:CloudKit 同步有延迟,不是实时的,别拿它做关键的状态判断。

最佳实践

已有项目迁移:如果你已经在用 TipKit,优先把那些”同时弹出多个 Tip”的热点页面用 TipGroup 重构。不需要改动现有的单个 Tip 逻辑,只需要把相关的 Tip 收进 Group 即可。自定义 Identifier 适合在你要做”内容驱动的 Tip”(新文章、新路线、新功能项)时引入。

新项目起步:建议一开始就规划好 TipGroup 的层级——哪些 Tip 是同一组的、用什么优先级策略。displayFrequency 建议从 .weekly 起步,给用户足够的自我探索时间。如果你有多设备场景,CloudKit 同步在第一天就开启,后面补救成本更高。

还有什么值得关注

  • TipViewStyle 协议让你完全自定义 Tip 的视觉样式,包括布局、字体、颜色,可以做出和 App 风格高度一致的引导卡片。
  • AccessoryWidgetGroup 提供了新的模板布局,适合在 watchOS 上展示最多三段内容的 Widget。
  • TipKit 的 displayFrequency 支持自定义 TimeInterval,不局限于预设的 daily/weekly,可以根据功能复杂度灵活设置间隔。
WWDC 2024