TipKit
使用 Apple 的 TipKit 框架,為 iOS 17+ 應用程式加入功能探索提示、上下文提示與引導操作標記。TipKit 會管理顯示頻率、符合資格規則與持久性,讓提示在正確時機出現,並在使用者學會功能後消失。
目錄
設定
在 App.init 中,於任何視圖渲染之前,呼叫 Tips.configure() 一次。這會初始化提示的資料儲存庫並開始規則評估。若在之後才呼叫,可能會發生提示視圖在資料儲存庫準備好之前就嘗試顯示的競爭情況。
import SwiftUI
import TipKit
@main
struct MyApp: App {
init() {
try? Tips.configure([
.datastoreLocation(.applicationDefault)
])
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
DatastoreLocation 選項
| 選項 | 使用情境 |
|---|---|
.applicationDefault | 預設位置,應用程式沙盒(適用於多數應用程式) |
.groupContainer(identifier:) | 在應用程式與其擴充功能之間共享提示狀態 |
.url(_:) | 自訂檔案 URL,完全控制儲存位置 |
CloudKit 同步
跨使用者的裝置同步提示狀態,避免使用者在每台裝置上都看到相同的提示。將 CloudKit 容器選項與資料儲存庫位置一起加入。
try? Tips.configure([
.datastoreLocation(.applicationDefault),
.cloudKitContainer(.named("iCloud.com.example.app"))
])
定義提示
建立一個符合 Tip 協定的結構。至少需要提供 title。並可加入 message 作為輔助細節,以及 image 作為前置圖示。保持標題簡短且以行動為導向,因為提示會以緊湊的呼叫輸出形式出現。
import TipKit
struct FavoriteTip: Tip {
var title: Text { Text("釘選你的最愛") }
var message: Text? { Text("點擊愛心圖示即可儲存項目以供快速存取。") }
var image: Image? { Image(systemName: "heart") }
}
屬性: title(必要)、message(選擇性細節)、image(選擇性前置圖示)、actions(選擇性按鈕)、rules(選擇性符合資格條件)、options(顯示頻率、最大次數)。
生命週期: 待定(規則未滿足)-> 符合資格(所有規則通過)-> 失效(被關閉、執行動作或程式化移除)。一旦失效,除非重設資料儲存庫,否則提示不會再次出現。
顯示提示
內嵌提示 TipView
將 TipView 直接嵌入你的佈局中。它會渲染成一個圓角卡片,並以動畫效果出現和消失。適用於可滾動內容中的提示。
let favoriteTip = FavoriteTip()
var body: some View {
VStack {
TipView(favoriteTip)
ItemListView()
}
}
彈出提示 .popoverTip()
將提示作為彈出視窗,錨定在任何視圖上。框架會繪製一條從彈出視窗指向錨定物的箭頭。適用於指向特定控制項的提示。
Button { toggleFavorite() } label: { Image(systemName: "heart") }
.popoverTip(favoriteTip)
// 控制箭頭方向(省略則由系統選擇)
.popoverTip(favoriteTip, arrowEdge: .bottom)
自訂 TipViewStyle
建立自訂樣式以控制整個應用程式的提示外觀。遵循 TipViewStyle 並實作 makeBody(configuration:)。
struct CustomTipStyle: TipViewStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(spacing: 12) {
configuration.image?
.font(.title2)
.foregroundStyle(.tint)
VStack(alignment: .leading, spacing: 4) {
configuration.title
.font(.headline)
configuration.message?
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.padding()
}
}
// 全局應用或逐視圖應用
TipView(favoriteTip)
.tipViewStyle(CustomTipStyle())
提示規則
規則控制提示何時符合資格。rules 陣列中的所有規則都必須通過,提示才會顯示。TipKit 支援兩種規則類型:基於參數和基於事件。
基於參數的規則
使用 @Parameter 來追蹤應用程式狀態。當參數值滿足規則條件時,提示即符合資格。
struct FavoriteTip: Tip {
@Parameter
static var hasSeenList: Bool = false
var title: Text { Text("釘選你的最愛") }
var rules: [Rule] {
#Rule(Self.$hasSeenList) { $0 == true }
}
}
// 當使用者到達列表時設定參數
FavoriteTip.hasSeenList = true
基於事件的規則
使用 Tips.Event 來追蹤使用者動作。每次動作發生時貢獻給該事件。當貢獻次數或時間條件滿足時,規則就會觸發。這非常適合用於提示應該在使用者執行某個動作數次但尚未發現相關功能後才出現的情況。
struct ShortcutTip: Tip {
static let appOpenedEvent = Tips.Event(id: "appOpened")
var title: Text { Text("試試快速動作") }
var rules: [Rule] {
#Rule(Self.appOpenedEvent) { $0.donations.count >= 3 }
}
}
// 每次應用程式開啟時貢獻
ShortcutTip.appOpenedEvent.donate()
組合多個規則
將多個規則放入陣列中。所有規則都必須通過(邏輯 AND)。
struct AdvancedTip: Tip {
@Parameter
static var isLoggedIn: Bool = false
static let featureUsedEvent = Tips.Event(id: "featureUsed")
var title: Text { Text("解鎖進階模式") }
var rules: [Rule] {
#Rule(Self.$isLoggedIn) { $0 == true }
#Rule(Self.featureUsedEvent) { $0.donations.count >= 5 }
}
}
顯示頻率選項
使用 options 屬性控制提示的顯示頻率。
struct DailyTip: Tip {
var title: Text { Text("每日提醒") }
var options: [TipOption] {
MaxDisplayCount(3) // 最多總共顯示 3 次
IgnoresDisplayFrequency(true) // 繞過全局頻率限制
}
}
全局顯示頻率在設定時指定:
try? Tips.configure([
.displayFrequency(.daily) // .immediate, .hourly, .daily, .weekly, .monthly
])
使用 .daily 時,系統每天在整個應用程式中最多顯示一個提示,除非特定提示設定了 IgnoresDisplayFrequency(true)。
提示動作
為提示加入動作按鈕以進行直接互動。每個動作都有一個 id 和一個標籤。在提示視圖的動作處理器中處理該動作。
struct FeatureTip: Tip {
var title: Text { Text("試試新的編輯器") }
var message: 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":
navigateToEditor()
featureTip.invalidate(reason: .actionPerformed)
case "learn-more":
showHelpSheet = true
default:
break
}
}
提示群組
使用 TipGroup 來協調單一視圖中的多個提示。TipGroup 確保一次只顯示群組中的一個提示,防止提示過載。提示會依照優先順序顯示。
struct OnboardingView: View {
let tipGroup = TipGroup(.ordered) {
WelcomeTip()
NavigationTip()
ProfileTip()
}
var body: some View {
VStack {
if let currentTip = tipGroup.currentTip {
TipView(currentTip)
}
Button("下一步") {
tipGroup.currentTip?.invalidate(reason: .actionPerformed)
}
}
}
}
優先順序選項
| 初始化器 | 行為 |
|---|---|
.ordered | 提示依照列出的順序顯示 |
當目前的提示失效後,群組中下一個符合資格的提示會成為 currentTip。
程式化控制
使提示失效
當使用者執行了提示描述的動作,或提示不再相關時,呼叫 invalidate(reason:)。
let tip = FavoriteTip()
tip.invalidate(reason: .actionPerformed)
| 原因 | 使用時機 |
|---|---|
.actionPerformed | 使用者執行了提示描述的動作 |
.displayCountExceeded | 提示已達到最大顯示次數 |
.tipClosed | 使用者明確關閉了提示 |
測試工具
TipKit 提供了靜態方法來控制在開發和測試期間的提示可見性。將它們放在 #if DEBUG 或 ProcessInfo 檢查之後,確保它們永遠不會在正式建置中執行。
#if DEBUG
// 顯示所有提示,無論規則為何(在開發期間很有用)
Tips.showAllTipsForTesting()
// 僅顯示特定提示
Tips.showTipsForTesting([FavoriteTip.self, ShortcutTip.self])
// 隱藏所有提示(對於不涉及提示的 UI 測試很有用)
Tips.hideAllTipsForTesting()
// 重設資料儲存庫(清除所有提示狀態、失效記錄和事件)
try? Tips.resetDatastore()
#endif
使用 ProcessInfo 進行測試方案
if ProcessInfo.processInfo.arguments.contains("--show-all-tips") {
Tips.showAllTipsForTesting()
}
在開發建置的 Xcode 方案中,將 --show-all-tips 作為啟動引數傳遞。
常見錯誤
不要:在 App.init 以外的任何地方呼叫 Tips.configure()
在視圖的 onAppear 或 task 修飾器中呼叫 Tips.configure() 會產生競爭情況,導致提示視圖在資料儲存庫準備好之前就嘗試渲染,造成提示遺失或閃爍。
// 錯誤
struct ContentView: View {
var body: some View {
Text("Hello")
.task { try? Tips.configure() } // 太遲了,視圖已渲染
}
}
// 正確
@main struct MyApp: App {
init() { try? Tips.configure() }
var body: some Scene { WindowGroup { ContentView() } }
}
不要:一次顯示太多提示
同時顯示多個提示會讓使用者感到不知所措,並削弱每個提示的影響力。使用者會學會忽略它們。
// 錯誤:三個提示同時可見
VStack {
TipView(tipA)
TipView(tipB)
TipView(tipC)
}
// 正確:使用 TipGroup 將它們排序
let group = TipGroup(.ordered) { TipA(); TipB(); TipC() }
if let currentTip = group.currentTip {
TipView(currentTip)
}
不要:忘記在使用者執行動作後使提示失效
如果提示說「點擊星號加入最愛」,而使用者點擊了星號但提示仍然存在,這會削弱使用者對 UI 的信任。
// 錯誤:使用者操作後提示仍然可見
Button("最愛") { toggleFavorite() }
.popoverTip(favoriteTip)
// 正確:在操作時使提示失效
Button("最愛") {
toggleFavorite()
favoriteTip.invalidate(reason: .actionPerformed)
}
.popoverTip(favoriteTip)
不要:在正式版本中啟用測試提示
Tips.showAllTipsForTesting() 會繞過所有規則和頻率限制。如果在正式版本中啟用,每個使用者都會立即看到所有提示。
// 錯誤:始終啟用
Tips.showAllTipsForTesting()
// 正確:放在 DEBUG 後面
#if DEBUG
Tips.showAllTipsForTesting()
#endif
不要:讓提示標題過長
長標題在緊湊的提示呼叫輸出中會被截斷或怪異地換行。將關鍵動作放在標題中,將輔助上下文放在訊息中。
// 錯誤
var title: Text { Text("你可以點擊愛心按鈕將此項目儲存到你的最愛清單") }
// 正確
var title: Text { Text("儲存到最愛") }
var message: Text? { Text("點擊愛心圖示即可保留項目以供快速存取。") }
不要:將提示用於關鍵資訊
使用者可以隨時關閉提示,且關閉後不會再次出現。絕對不要將必要的說明或安全資訊放在提示中。
// 錯誤:關鍵資訊放在可關閉的提示中
struct DataLossTip: Tip {
var title: Text { Text("未儲存的變更將會遺失") }
}
// 正確:對關鍵資訊使用警示或內聯警告
// 保留提示給功能探索和漸進式揭露
審查清單
-
Tips.configure()在App.init中呼叫,且在渲染任何視圖之前 - 每個提示都有清晰簡潔的標題(以行動為導向,約 40 個字元以內)
- 當使用者執行提示描述的動作時,提示已失效
- 設定了規則,以便提示在正確時機出現(而非所有提示都在首次啟動時立即顯示)
- 當一個視圖中存在多個提示時,使用了
TipGroup - 測試工具(
showAllTipsForTesting、resetDatastore)放在#if DEBUG後面 - 如果應用程式支援多個裝置,已設定 CloudKit 同步
- 顯示頻率設定適當(大多數應用程式使用
.daily或.weekly) - 提示僅用於功能探索,而非關鍵資訊
- 如果預設樣式不符合應用程式設計,則一致地套用了自訂
TipViewStyle - 已處理提示動作,並在動作處理器中使提示失效
- 事件貢獻已放置在正確的使用者動作點
- 確保自訂 Tip 類型符合 Sendable;在 @MainActor 上設定 Tips
參考資料
- 參閱 references/tipkit-patterns.md 了解完整的實作模式,包含自訂樣式、基於事件的規則、提示群組、測試策略、引導流程及 SwiftUI 預覽設定。