大多数浏览器和
Developer App 均支持流媒体播放。
-
使用 TipKit 自定功能探索
TipKit 框架旨在改进功能发现体验,让你可以轻松地在 App 中显示相关提示。现在,你可以将提示分组以便用户按照理想的顺序发现相应的功能、借助自定提示标识符使提示可以重复使用、与自己 App 的外观和使用感受完美契合,还可使用 CloudKit 同步提示。了解如何利用 TipKit 的最新改进,帮助用户发现你 App 提供的各项功能。
章节
- 0:00 - Introduction
- 1:18 - Tip groups
- 5:12 - Reusable tips with custom identifiers
- 8:25 - Custom tip styles
- 10:48 - Sync tips with CloudKit
资源
相关视频
WWDC23
-
下载
大家好 我是 Jake 我非常高兴 能与大家分享一些 自定 TipKit 的新方法 帮助用户了解你的 App 中 未被发现的新功能
TipKit 是一个框架 可轻松 在 App 中显示提示 借助这个框架 你可以向用户介绍全新功能 或展示更快完成任务的方法 使用 TipKit 可轻松创建提示 并自动管理提示的 显示状态和历史记录 确保只在适当的时刻显示提示 利用 TipKit 还能设置条件规则 和显示频率 控制何时向哪些用户显示提示 此外 TipKit 还提供 各种呈现样式 不仅能与 App 的 UI 完美契合 而且适用于所有平台 现在还有更多方法 来自定功能发现 让你的提示为用户带来 全面整合的丝滑体验 在本视频中 我将介绍如何将提示分组 以便让用户按理想的顺序 发现各项功能 展示如何通过自定提示标识符 让提示可重复使用 使用 TipViewStyle 让提示与 App 的外观和使用感受保持一致 还将介绍如何使用 CloudKit 来同步 TipKit 数据存储 以便跨设备共享 提示显示状态
首先来看看 提示组 提示组允许你指定多个提示 并按特定顺序 逐个显示 或者显示第一个符合条件的提示 我们将更新 App 帮助野外徒步者发现新路线 并进行导航 我最近在地图上添加了 一个指南针控件 这个控件有两个功能 我们要 使用提示让用户了解这些功能 我们要创建的 第一个提示是轻点指南针 即可在地图上显示当前位置 因此 我们将新建一个提示结构 使用标题、信息和图片来描述功能
我们的第二个提示是 一个不太容易发现的手势 指南针控件还支持长按手势 长按可将地图旋转回北纬 0 度 因此我们也要添加一个提示 来介绍一下这项功能
为显示这些提示 我们将从指南针向 TipKit 的 popoverTip 视图修饰符 发起两次调用 一个用于 showLocationTip
另一个用于 rotateMapTip
这些弹出窗口效果很好 但有一个问题 目前 我们无法控制 提示的显示顺序 但我们非常希望先向用户介绍 轻点指南针这项功能 然后再显示关于长按指南针的提示 因此 让我们使用 TipGroup 来更新代码
为控制提示的显示方式 我们将两个指南针提示 都添加到 TipGroup 中 使用 ordered 优先级 进行初始化 使用 ordered 优先级 可确保 RotateMapTip 不会显示 除非 ShowLocationTip 失效 也就是用户取消了 ShowLocationTip 视图 或执行了显示位置的 轻点手势 现在 我们只需更新 指南针的 popoverTip 视图修饰符 就能使用 currentTip 属性 来显示组中当前可用的提示
TipGroup 的 currentTip 属性 还可以转换为特定类型 以便自定提示的显示位置 如果我们为指南针的两个功能 都设置了单独的按钮 我们可以使用 currentTip as? ShowLocationTip 这样可确保提示的弹出窗口 只能在第一个控件中显示 而 RotateMapTip 弹出窗口 只能在第二个控件中显示
现在 两个指南针提示 都能按正确的顺序显示 接下来就只剩一项任务了 那就是在用户使用了 提示中介绍的功能后 需要使这两项提示失效 使提示失效可确保 在用户已经发现功能后 不再向用户显示提示 此外 对于使用 ordered 优先级的 TipGroups 只有前面的提示全部失效后 才能显示后面的提示 TipGroups 可配置 ordered 优先级 或 firstAvailable 优先级 ordered 优先级 比如 compassTips 使用的优先级 非常适合向用户逐步介绍相关功能
firstAvailable 优先级 会显示第一个满足显示规则的提示 如果视图中有多个不相关的提示 但每次只想显示一个时 这种优先级就非常有用 TipGroups 与 displayFrequency 搭配使用的效果也很棒 我们的野外徒步路线 App 将提示的 displayFrequency 配置为每周 这样就可以先留出一些时间 让探路者自行探索指南针的功能 然后再向他们展示相关提示 TipKit 的 displayFrequency 是很不错的方法 可避免用户在首次启动 App 时 收到过多提示 提示组非常适合按照需要的顺序 逐一显示提示
综合使用提示组 以及显示规则和显示频率 你可以逐步显示相关的功能 而不会导致 App 被太多提示塞满
现在我们谈谈如何使用自定标识符 让提示可重复使用 每个提示的标识符决定了 提示的状态和规则都是唯一的 覆盖提示的默认标识符 就可以根据提示内容 重复使用相同的提示结构 我们的野外徒步路线 App 在最近的更新中 添加了对一条新路线的支持 我们希望通过提示 让用户了解这条新路线 首先为新添加的 Butler Fork 路线起点创建一个提示 说明路线起点的位置 我们还要添加一个操作按钮 这样用户就可以在地图上 轻松导航到新的路线 此外 由于我们希望 只有最需要使用这条提示的 徒步者才会看到这条提示 所以我们要添加一条事件规则 只有当用户访问过这个路线区域 3 次或 3 次以上时 才会向用户显示提示
要显示提示 我们只需将提示实例 添加到我们的 TrailList 然后使用 TipView 来显示 我们还要添加一个操作处理程序 在用户轻点“Go there now”按钮时 突出显示新路线
但是 如果以后我们要添加 对更多新路线的支持 效果会怎样呢? 啊哦! 如果我们继续在 App 中添加新路线 我们的 TrailList 代码基本 都会变成提示 此外 多个提示视图 可能会同时出现 导致用户难以访问实际的路线列表 这种方法的可扩展性不强 因此让我们更新代码 创建自定标识符 实现可重复使用的提示
首先 我们定义一个新提示 使用特定路线对象创建提示 并根据路线名称和区域 为提示添加信息 接下来 根据用于初始化提示的路线 为提示添加一个自定 ID 通过自定这个 ID NewTrailTip 的每个实例 基于所描述的路线 都将拥有唯一的状态和规则 这样 即使提示之前已经 因其他路线而失效 也可以通过新路线重新显示 为确保这些提示仍然可以 显示给合适的受众 也就是 只向那些对所述区域 感兴趣的徒步者显示 我们将更新 didVisit 显示规则 让这个规则基于新添加路线 所在的区域来运行
现在 我们只需更改 TrailList 代码 即可根据最新的路线创建新提示 这样 无论何时 在 App 添加新路线 都会自动显示提示 由于我们只创建一个提示实例 因此不必担心 同时出现多个 NewTrailTip 根据提示标识符 每条提示都有一条持久记录 即使提示从未显示过 这样 TipKit 就能根据 多次启动 App 时的情况 使提示符合条件 因此 当你指定自定标识符时 一定要基于用户 ID 或路线名称 等具体信息来指定
默认情况下 提示的标识符是 用于初始化提示的类型名称 覆盖这个标识符可以 根据内容重复使用提示
借助自定标识符 你可以轻松 通过 TipKit 为不同提示 重复使用同一提示模型
接下来 我们谈谈自定 提示视图的外观
提示的默认呈现形式不错 但在某些情况下 你可能需要更深入的自定 以便更好地匹配 App 的 UI 对于这些提示 你可以使用 TipViewStyle 自定提示的外观和行为 我们在徒步 App 中添加的每条路线 都有非常漂亮的照片 我认为应该使用 NewTrailTip 来展示这些照片 因此 让我们创建一个 自定 TipViewStyle 使用每条路线的主题图片作为背景 并在叠层中显示提示的标题和信息
对于标题和信息 我们将使用 makeBody 函数的 配置参数中的属性 而不是提示的实例值 这样 我们应用于 TipView 的 任何修饰符 都能与自定样式中的 信息和标题搭配使用
应用时 我们只需 调用 tipViewStyle 修饰符 现在 我们的提示以路线的 绚丽照片作为背景 但是 我们的 NewTrailTip 还包含一个操作 在地图上快速突出显示路线 我不想在照片上放置一个按钮
那么 让我们更新自定样式 使整个提示视图都可以轻点
我们先从配置参数中 获取 NewTrailTip 的操作 现在 我们只需在轻点提示视图时 调用操作处理程序 使用配置参数中的操作属性 可确保 TipView 中 创建的处理程序 在执行操作时仍会被调用
创建自定 TipViewStyle 时 务必尽可能使用配置参数中的属性 而不是提示的实例值
这样 在使用自定样式时 应用于 TipViews 的 闭包和修饰符仍可接受评估
自定 TipViewStyle 也可搭配 其他提示视图修饰符 如 tipCornerRadius 和 tipBackground 对于使用 UIKit 或 AppKit 的 App 可以设置 viewStyle 属性 来更改 TipUIView 和 TipNSView 的样式 通过创建 TipViewStyle 可轻松显示具有 自定外观和行为的提示 同时 TipKit 的规则引擎仍然可以 处理提示的显示和关闭
现在 让我们来谈谈 CloudKit 同步 CloudKit 可以同步 提示的显示状态 从而帮你改善 App 的用户体验 并确保用户无需在多台设备上 关闭同一提示 现在 我们已经为野外徒步路线 App 添加了一些很棒的提示 我们应该设置 CloudKit 同步 这样我们的提示状态 和规则就可以共享了 首先 找到我们的 Xcode 项目 在“Signing & Capabilities” 标签页下添加 iCloud 功能 然后 我们将打开 iCloud 服务下的 CloudKit 并为同步提示创建一个新容器 我们还需要添加 Background Modes 并启用 “Remote notifications”功能 这样 TipKit 可在后台处理 远程更改 以确保 App 的路线提示 始终保持正确的状态和显示状态
最后一步 我们只需更新提示配置调用 添加 cloudKitContainer 选项 并传入新容器的 ID
这样就可以了 现在 我们的路线提示将在 不同设备间保持同步 因此用户无需多次关闭同一提示 此外 由于 TipKit 还会同步 事件和参数值 因此在一台设备上 触发 NewTrailTip 显示的传递数据 也可以共享 从而允许提示在其他设备上显示
App 中的提示在失效前 可能只会出现几次 通过持久化提示 TipKit 可以避免 在准备好显示提示之前 将提示模型载入内存 通过 CloudKit 同步 提示的状态和规则可以共享 因此在一台设备上关闭提示后 就不会在另一台设备上重新显示
TipKit 还能同步事件和参数 让你可以根据 多台设备间的事件传递 来创建显示规则 显示次数和持续时间值也会同步 因此指定 MaxDisplayCount 的提示 可以根据提示在所有设备上 出现的总次数而失效
在某些情况下 你可能希望使用 CloudKit 同步 但同时又希望一些提示 在不同平台上有所不同 对于这种情况 你可以使用 UIDevice 创建特定于平台的提示 ID 以便在多台设备上 重新显示相同的提示
为便于测试 TipKit 的 resetDatastore 函数 将清除本地数据存储 以及 CloudKit 中所有提示的记录
TipKit 建立在 SwiftData 强大的持久性之上 这使得提示状态、规则、参数 和事件可以在各次 App 启动间保留提示值 利用 CloudKit 同步功能 在设备间共享这些值变得更容易了
TipKit 拥有功能强大的工具 可确保 App 的提示 只在最合适的时间 显示给最需要查看这些提示的受众 使用 TipGroup 可以让用户 按理想的顺序发现 App 的功能 一次发现一个功能 不会造成困扰 提示组与显示规则和显示频率 配合使用 可在 App 中自定功能发现 要了解有关创建显示规则的 更多信息 请观看去年的 WWDC 视频 “使用 TipKit 以提升功能的可发现性”
自定标识符让你可以轻松制作 可重复使用的提示模型 这样就可以根据 提示内容重新显示提示 TipViewStyle 可用于 为提示创建自定布局 和交互 使提示始终 与你 App 的 UI 相匹配 CloudKit 可用于在不同设备间 同步 TipKit 的数据存储 从而避免提示不必要地重新显示
我谨代表整个 TipKit 团队 感谢大家今天的参与 我们无比期待 TipKit 能够更好地帮助用户 发现你的 App 的新功能!
-
-
1:43 - Create new tips
// Create new tips struct ShowLocationTip: Tip { var title: Text { Text("Show your location") } var message: Text? { Text("Tap the compass to highlight your current location on the map.") } var image: Image? { Image(systemName: "location.circle") } }
-
1:54 - Create new tips
// Create new tips struct ShowLocationTip: Tip { var title: Text { Text("Show your location") } var message: Text? { Text("Tap the compass to highlight your current location on the map.") } var image: Image? { Image(systemName: "location.circle") } } struct RotateMapTip: Tip { var title: Text { Text("Reorient the map") } var message: Text? { Text("Tap and hold on the compass to rotate the map back to 0° North.") } var image: Image? { Image(systemName: "hand.tap") } }
-
2:09 - Show popover tips
// Show popover tips struct MapCompassControl: View { let showLocationTip = ShowLocationTip() let rotateMapTip = RotateMapTip() var body: some View { CompassDial() .popoverTip(showLocationTip) .popoverTip(rotateMapTip) .onTapGesture { showCurrentLocation() } .onLongPressGesture(minimumDuration: 0.1) { reorientMapHeading() } } }
-
2:41 - Create a TipGroup
// Create a TipGroup struct MapCompassControl: View { @State var compassTips: TipGroup(.ordered) { ShowLocationTip() RotateMapTip() } var body: some View { CompassDial() .popoverTip(compassTips.currentTip) .onTapGesture { showCurrentLocation() } .onLongPressGesture(minimumDuration: 0.1) { reorientMapHeading() } } }
-
3:15 - Show TipGroup tips on different views
// Show TipGroup tips on different views struct MapControlsStack: View { @State var compassTips: TipGroup(.ordered) { ShowLocationTip() RotateMapTip() } var body: some View { VStack { ShowLocationButton() .popoverTip(compassTips.currentTip as? ShowLocationTip) RotateMapButton() .popoverTip(compassTips.currentTip as? RotateMapTip) } } }
-
3:50 - Invalidate tips
// Invalidate tips struct MapCompassControl: View { @State var compassTips: TipGroup(.ordered) { showLocationTip rotateMapTip } var body: some View { CompassDial() .popoverTip(compassTips.currentTip) .onTapGesture { showLocationTip.invalidate(reason: .actionPerformed) showCurrentLocation() } .onLongPressGesture(minimumDuration: 0.1) { rotateMapTip.invalidate(reason: .actionPerformed) reorientMapHeading() } } }
-
5:37 - Create a tip
// Create a tip struct ButlerForkTip: Tip { var title: Text { Text("Butler Fork is now available") } var message: Text? { Text("To see key trail info, tap Big Cottonwood Canyon on the map.") } var actions: [Action] { Action(title: "Go there now") } var rules: [Rule] { #Rule(Region.bigCottonwoodCanyon.didVisitEvent) { $0.donations.count > 3 } } }
-
6:01 - Show a TipView
// Show a TipView struct ButlerForkTip: Tip { var title: Text { Text("Butler Fork is now available") } var message: Text? { Text("To see key trail info, tap Big Cottonwood Canyon on the map.") } var actions: [Action] { Action(title: "Go there now") } var rules: [Rule] { #Rule(Region.bigCottonwoodCanyon.didVisitEvent) { $0.donations.count > 3 } } } struct TrailList: View { var trails: [Trail] var body: some View { ScrollView { let butlerForkTip = ButlerForkTip() TipView(butlerForkTip) { _ in highlightButlerForkTrail() } ListSection(title: "Trails", trails: trails) } } }
-
6:45 - Create a reusable tip
// Create a reusable tip struct NewTrailTip: Tip { let newTrail: Trail var title: Text { Text("\(newTrail.name) is now available") } var message: Text? { Text("To see key trail info, tap \(newTrail.region) on the map.") } var actions: [Action] { Action(title: "Go there now") } var id: String { "NewTrailTip-\(newTrail.id)" } var rules: [Rule] { #Rule(newTrail.region.didVisitEvent) { $0.donations.count > 3 } } }
-
7:26 - Show a TipView
// Show a TipView struct NewTrailTip: Tip { let newTrail: Trail var title: Text { Text("\(newTrail.name) is now available") } var message: Text? { Text("To see key trail info, tap \(newTrail.region) on the map.") } var actions: [Action] { Action(title: "Go there now") } var id: String { "NewTrailTip-\(newTrail.id)" } var rules: [Rule] { #Rule(newTrail.region.didVisitEvent) { $0.donations.count > 3 } } } struct TrailList: View { var trails: [Trail] let newTrail: Trail var body: some View { ScrollView { let newTrailTip = NewTrailTip(newTrail: newTrail) TipView(newTrailTip) { _ in highlightTrail(newTrailTip) } ListSection(title: "Trails", trails: trails) } } }
-
8:55 - Create a custom TipViewStyle
// Create a custom TipViewStyle struct NewTrailTipViewStyle: TipViewStyle { func makeBody(configuration: Configuration) -> some View { let tip = configuration.tip as! NewTrailTip TrailImage(imageName: tip.newTrail.heroImage) .frame(maxHeight: 150) .overlay { VStack { configuration.title.font(.title) configuration.message.font(.subheadline) } } } } extension NewTrailTipViewStyle { struct TrailImage: View { let imageName: String var body: some View { Image(imageName) .resizable() .aspectRatio(contentMode: .fill) } } }
-
9:20 - Apply a TipViewStyle
// Apply a TipViewStyle struct NewTrailTipViewStyle: TipViewStyle { func makeBody(configuration: Configuration) -> some View { let tip = configuration.tip as! NewTrailTip TrailImage(imageName: tip.newTrail.heroImage) .frame(maxHeight: 150) .overlay { VStack { configuration.title.font(.title) configuration.message.font(.subheadline) } } } } extension NewTrailTipViewStyle { struct TrailImage: View { let imageName: String var body: some View { Image(imageName) .resizable() .aspectRatio(contentMode: .fill) } } } struct TrailList: View { var trails: [Trail] let newTrail: Trail var body: some View { ScrollView { let newTrailTip = NewTrailTip(newTrail: newTrail) TipView(newTrailTip) { _ in highlightTrail(newTrailTip) } .tipViewStyle(NewTrailTipViewStyle()) ListSection(title: "Trails", trails: trails) } } }
-
9:45 - Add the tip's action handler
// Apply a TipViewStyle struct NewTrailTipViewStyle: TipViewStyle { func makeBody(configuration: Configuration) -> some View { let tip = configuration.tip as! NewTrailTip let highlightTrailAction = configuration.actions.first! TrailImage(imageName: tip.newTrail.heroImage) .frame(maxHeight: 150) .onTapGesture { highlightTrailAction.handler() } .overlay { VStack { configuration.title.font(.title) HStack { configuration.message.font(.subheadline) Spacer() Image(systemName: "chevron.forward.circle") .foregroundStyle(.white) } } } } } extension NewTrailTipViewStyle { struct TrailImage: View { let imageName: String var body: some View { Image(imageName) .resizable() .aspectRatio(contentMode: .fill) } } } struct TrailList: View { var trails: [Trail] let newTrail: Trail var body: some View { ScrollView { let newTrailTip = NewTrailTip(newTrail: newTrail) TipView(newTrailTip) { _ in highlightTrail(newTrailTip) } .tipViewStyle(NewTrailTipViewStyle()) ListSection(title: "Trails", trails: trails) } } }
-
11:38 - Add CloudKit sync for tips
// Add CloudKit sync for tips @main struct TipKitTrails: App { var body: some Scene { WindowGroup { ContentView() .task { await configureTips() } } } func configureTips() async { do { try Tips.configure([ .cloudKitContainer(.named("iCloud.com.apple.TipKitTrails.tips")), .displayFrequency(.weekly) ]) } catch { print("Unable to configure tips: \(error)") } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。