核心摘要:默认情况下,TipKit 的提示显示顺序是不可控的。在 iOS 18 中,苹果引入了 TipGroup API,专门用于构建有序的 Onboarding(新手引导)流程。对于旧版本,我们可以通过自定义规则巧妙实现相同的效果。
TipKit 是帮助用户发现新功能的利器,但在构建分步教程(Step-by-step tutorial)时,我们需要严格控制提示的出现顺序。本文将介绍 iOS 18 原生方案与 iOS 17 兼容方案。
方案一:使用 TipGroup(iOS 18+ 推荐)
在 iOS 18 (以及对应的 macOS 15, iOS 26 环境) 中,TipGroup 是管理多个提示优先级的最佳方式。通过设置 .ordered 属性,系统会自动确保同一时间只显示一个提示,且前一个提示失效后才会显示下一个。
基础用法
Swift
struct Tip1: Tip {
@Parameter static var show: Bool = false
var title: Text { Text("第一步:点击这里") }
var rules: [Rule] {
#Rule(Self.$show) { $0 }
}
}
struct Tip2: Tip {
var title: Text { Text("第二步:查看详情") }
// Tip2 不需要额外的 Rule,TipGroup 会自动控制它的显示时机
}
struct TipGroupDemo: View {
// 创建一个有序的 TipGroup
@State var tips = TipGroup(.ordered) {
Tip1()
Tip2()
}
var body: some View {
VStack {
Text("Welcome")
.popoverTip(tips.currentTip) // 自动显示当前应该显示的提示
Button("开始引导") {
Tip1.show = true // 激活 Tip1,Tip2 会排队等待
}
.buttonStyle(.bordered)
}
}
}
效果演示
分布式显示(跨组件)
TipGroup 不仅限于单一视图。你可以将其传递给不同的组件,实现跨视图的引导流程:
Swift
var body: some View {
VStack(spacing: 80) {
Text("功能模块 A")
// 只有当 currentTip 是 Tip1 时才显示
.popoverTip(tips.currentTip as? Tip1)
Text("功能模块 B")
// 只有当 currentTip 是 Tip2 时才显示
.popoverTip(tips.currentTip as? Tip2)
Button("启动教程") {
Tip1.show = true
}
}
}
方案二:自定义顺序逻辑(iOS 17 兼容)
在 iOS 17 中,我们需要手动通过 @Parameter 和 invalidate 回调来编排顺序。核心逻辑是:当前一个 Tip 关闭时,自动修改下一个 Tip 的激活参数。
实现步骤
- 定义协议:统一管理 Tip 的显示开关。
- 自定义 Style:拦截关闭操作,触发下一个 Tip。
完整代码
Swift
import SwiftUI
import TipKit
// 1. 定义统一协议
protocol ShowTip: Tip {
static var show: Bool { get set }
}
// 2. 定义 Tip
struct Step1Tip: ShowTip {
var title: Text = Text("第一步")
@Parameter static var show: Bool = false
var rules: [Rule] { [ #Rule(Self.$show) { $0 == true } ] }
}
struct Step2Tip: ShowTip {
var title: Text = Text("第二步")
@Parameter static var show: Bool = false
var rules: [Rule] { [ #Rule(Self.$show) { $0 == true } ] }
}
// 3. 自定义样式,负责"接力"
struct ChainedTipStyle<NextTip: ShowTip>: TipViewStyle {
let nextTip: NextTip.Type
func makeBody(configuration: Configuration) -> some View {
VStack(alignment: .leading) {
configuration.title?.font(.headline)
configuration.message?.font(.subheadline)
}
.padding()
.background(.thinMaterial)
.cornerRadius(10)
.overlay(alignment: .topTrailing) {
Button(action: {
// 关闭当前 Tip
configuration.tip.invalidate(reason: .tipClosed)
// 核心:激活下一个 Tip
nextTip.show = true
}) {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.padding(8)
}
}
}
// 4. 使用
struct LegacySequenceDemo: View {
let tip1 = Step1Tip()
let tip2 = Step2Tip()
var body: some View {
VStack {
Button("开始引导") { Step1Tip.show = true }
Text("目标 A")
.popoverTip(tip1)
// 绑定 Tip1 关闭后触发 Tip2
.tipViewStyle(ChainedTipStyle(nextTip: Step2Tip.self))
Text("目标 B")
.popoverTip(tip2)
}
}
}