作为我们秃头开发者来说,写出一款创意炸裂的 App 还不足以吸引用户眼球,更重要的是如何让用户用最短的时间掌握我们 App 的使用技巧。
从 iOS 17 开始, 推出了全新的 TipKit 框架专注于此事。有了它,我们再也不用自己写 App 用户帮助以及使用指南的逻辑和界面了。
使用 TipKit 非常简单,接下来就让我们一起走进 TipKit 的世界吧!
本文代码全部在 Xcode 15 beta8 上编译,在 iOS 17 beta8 上运行。
TipKit 是 在 WWDC 23 上推出的一款新框架,用于在界面显示提示(Tips)来帮助用户快速发掘我们 App 的使用特性。
目前该框架仍属于 beta 阶段,意味着它还有很多不确定性。
如果我没有记错, 直到 Xcode beta4 才将 TipKit 提供给开发者,而且现在 官网 TipKit 的示例代码在 Xcode beta8 中已提示语法错误了(我们后面会说明):
对于 这种习惯性“谜之”行为的更多细节,感兴趣的小伙伴们可以到如下链接中观赏:
按照 SwiftUI 的“习性”,一个 Tip 同时意味着外观和逻辑双重含义。
创建一个提示很简单,只需遵循 Tip 协议即可:
struct FavoriteTip: Tip {
var title: Text {
Text("收藏最爱的图片")
.bold()
}
var message: Text? {
Text("将心仪的图片保存到相册中")
.font(.headline)
.foregroundStyle(.gray.gradient)
}
}
Tip 协议还有很多其它可选属性,比如我们还可以为 Tip 界面进一步增加图片修饰:
struct FavoriteTip: Tip {
var image: Image? {
Image(systemName: "heart")
}
}
在 Tip 创建之后如何显示它们呢?有两种方式:嵌入和弹出。
我们可以直接将 Tip 嵌在视图中:
struct ContentView: View {
let favTip = FavoriteTip()
var body: some View {
NavigationStack {
VStack {
TipView(favTip)
}
.padding()
.navigationTitle("TitKit演示")
}
}
}
显示效果如下:
或者我们还可以将 Tip 直接依附于某一个视图,比如图片或按钮:
struct ContentView: View {
let favTip = FavoriteTip()
var body: some View {
NavigationStack {
VStack {...}
.padding()
.navigationTitle("TitKit演示")
.toolbar {
ToolbarItem {
Image(systemName: "heart")
.font(.title.weight(.black))
.foregroundStyle(.pink.gradient)
.popoverTip(favTip, arrowEdge: .top)
}
}
}
}
}
我们可以根据不同需求来组合使用这两种显示方式。
其实,TipKit 框架会在 App (本地目录)中存放一些相关的配置信息。理论上说,它们可能会通过 iCloud 同步到其它设备上去,这意味着在不同设备上相同 App 中的 TipKit 共享同一组配置。
我们可以进一步定制 TipKit 的配置细节,比如 Tip 显示频率、配置数据库保存的本地位置等等:
import SwiftUI
import TipKit
@main
struct TipKitTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
// Xcode 15 beta4 中过时的语法,目前已不能使用:
/*
try? await Tips.configure {
DisplayFrequency(.immediate)
DatastoreLocation(.applicationDefault)
}*/
}
}
}
}
如上代码所示,我们需要所有 Tip 立即显示,并且让系统决定配置数据存储的位置(我们也可以自己设置存储路径)。
在代码中,我们注释了之前官方示例中出错的代码片段,这些代码在 Xcode 15 beta8 中已不能使用。
除了全局 Tip 显示限制以外,我还可以设置单个 Tip 之间的显示规则。
比如,假设有两个提示,我们希望 FavoriteTip 在 StartTip 提示关闭后再显示,我们可以在 FavoriteTip 中用特定的规则(Rule)来表示这一约束:
struct FavoriteTip: Tip {
// 其它代码从略
var rules: [Rule] {
#Rule(Self.$startTipHasDisplayed) { $0 == true}
}
@Parameter
static var startTipHasDisplayed: Bool = false
}
现在,我们需要在 StartTip 提示关闭时将 FavoriteTip.startTipHasDisplayed 置为 true 才能触发 FavoriteTip 的显示:
struct ContentView: View {
let startTip = StartTip()
let favTip = FavoriteTip()
var body: some View {
NavigationStack {
VStack {
Image("1")
.resizable()
.aspectRatio(contentMode: .fill)
.onTapGesture {
// 关闭 startTip 提示
startTip.invalidate(reason: .actionPerformed)
// 触发 FavoriteTip 提升的显示
FavoriteTip.startTipHasDisplayed = true
}
TipView(startTip)
}
.padding()
.navigationTitle("TitKit演示")
.toolbar {
ToolbarItem {
Image(systemName: "heart")
.font(.title.weight(.black))
.foregroundStyle(.pink.gradient)
.popoverTip(favTip, arrowEdge: .top)
}
}
}
}
}
现在,只有等 StartTip 关闭后,FavoriteTip 提示才能显示出来:
有时,我们希望提示为用户提供更丰富的交互功能,比如在 Tip 中提供按钮跳转到更详细的使用教程界面。
TipKit 为此也提供了很好的支持,我们可以为 Tip 添加 Action 来驱动交互行为:
struct FavoriteTip: Tip {
// 其它代码从略
var actions: [Action] {
[
Tip.Action(id: "learn-more", title: "了解更多"),
Tip.Action(id: "forget", title: "下次再说")
]
}
}
在 Tip Action 被触发时,我们可以执行自定义行为:
struct ContentView: View {
let startTip = StartTip()
let favTip = FavoriteTip()
var body: some View {
NavigationStack {
VStack {
Image("1")
.resizable()
.aspectRatio(contentMode: .fill)
.onTapGesture {
startTip.invalidate(reason: .actionPerformed)
FavoriteTip.startTipHasDisplayed = true
}
TipView(startTip)
}
.padding()
.navigationTitle("TitKit演示")
.toolbar {
ToolbarItem {
Image(systemName: "heart")
.font(.title.weight(.black))
.foregroundStyle(.pink.gradient)
.popoverTip(favTip, arrowEdge: .top){ action in
switch action.index {
case 0:
favTip.invalidate(reason: .tipClosed)
// 跳转到详细使用教程
case 1:
favTip.invalidate(reason: .tipClosed)
// 直接退出提示
default:
break
}
}
}
}
}
}
}
现在,用户可以选择“了解更多”来进一步学习 App 的使用“秘技”了:
TipKit 全局配置存储在本地带有持久化特性,为了便于开发者即时测试, 提供了一些方法来快速显示或隐藏全部或指定 Tip:
一般的,要想在 Xcode 预览中正确测试 TipKit 的行为,我们需要在每次视图刷新时重置 TipKit 数据库,否则 Tip 不会正常显示:
#Preview {
ContentView()
.task {
// 在每次视图刷新时将 TipKit 数据库重置为初始状态
try? Tips.resetDatastore()
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
}
}
在本篇博文中,我们介绍了 SwiftUI 5.0(iOS 17)中新引进的开发框架 TipKit,使用它我们可以非常方便和快速的向用户介绍我们 App 中的各种特性和使用指南,小伙伴们还不快操练起来!
感谢观赏,再会!