tipkit

tipkit

熱門

實作與審查 Apple TipKit 功能探索 UI,適用於 iOS 17+ 的 App。用於新增或稽核應用程式內提示、情境說明、引導標記、Tip、TipView、popoverTip、規則、事件、動作、顯示頻率、測試覆寫、可重複使用的提示識別碼,或 iOS 18+ 的 TipGroup 與 CloudKit 提示同步;避免用於 TipKit 提示呈現以外的通用 SwiftUI 導覽或佈局。

936星標
47分支
更新於 2026/7/15
SKILL.md
唯讀
名稱
tipkit
描述

實作與審查 Apple TipKit 功能探索 UI,適用於 iOS 17+ 的 App。用於新增或稽核應用程式內提示、情境說明、引導標記、Tip、TipView、popoverTip、規則、事件、動作、顯示頻率、測試覆寫、可重複使用的提示識別碼,或 iOS 18+ 的 TipGroup 與 CloudKit 提示同步;避免用於 TipKit 提示呈現以外的通用 SwiftUI 導覽或佈局。

TipKit

使用 TipKit 來呈現小型、情境相關的功能探索時刻:內嵌提示、彈出提示、規則驅動的教學,以及輕量引導標記。除非 TipKit 呈現是核心問題,否則請將通用的 SwiftUI 架構、導覽、佈局以及較長的新手引導流程保留在相關的兄弟技能中。

目錄

可用性

TipKit 的核心 TipTipViewpopoverTip、規則、事件、選項以及測試覆寫功能,適用於 iOS 17+、iPadOS 17+、macOS 14+、tvOS 17+、watchOS 10+ 以及 visionOS 1+。

請明確標記較新的 API:

API 可用性 用途
TipGroup iOS 18+ 分組或排序提示;套用提示群組的決策。
.cloudKitContainer(...) iOS 18+ 跨裝置同步提示狀態、參數、事件與顯示次數。
MaxDisplayDuration iOS 18+ 累計顯示時間後自動失效。
resetEligibility() iOS 26+ 讓先前已失效的提示重新符合資格,無需重設資料儲存。

設定 TipKit

在應用程式初始化時呼叫 Tips.configure(_:) 一次,且必須在任何提示顯示之前完成。請勿在檢視的 onAppear.task 中設定 TipKit。

import SwiftUI
import TipKit

@main
struct MyApp: App {
    init() {
        do {
            try Tips.configure([
                .datastoreLocation(.applicationDefault),
                .displayFrequency(.daily)
            ])
        } catch {
            assertionFailure("TipKit 設定失敗:\(error)")
        }
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

僅在應用程式與擴充功能或應用程式群組成員之間有意共享提示狀態時,才使用 .datastoreLocation(.groupContainer(identifier:))。請保持應用程式群組成員之間的選項設定一致,因為 TipKit 會將選項狀態與提示記錄一起持久化。

CloudKit 同步

僅在 iOS 18+ 及更新版本中使用 CloudKit 同步。啟用 iCloud + CloudKit 以及背景模式 > 遠端通知,然後傳入容器:

try Tips.configure([
    .cloudKitContainer(.named("iCloud.com.example.app.tips"))
])

建議使用專用容器並加上 .tips 後綴。.automatic 會優先使用第一個符合資格的 .tips 容器,若無則回退至主要容器。

設計良好的提示

提示是小型、暫時性的輔助說明。用於使用者能理解並在幾個簡單步驟內嘗試的功能。如果流程需要長篇解釋、多個畫面或重要的安全/錯誤資訊,請改用教學、警示、內嵌警告或引導流程。

遵循 HIG 一致的預設值:

  • 保持標題簡短、直接且以行動為導向。
  • 使用一兩句話;避免促銷或不相關的內容。
  • 將提示放置在所說明的功能附近。
  • 當隱藏附近 UI 會中斷任務時,優先使用內嵌提示。
  • 當需要保留當前佈局且提示能指向特定控制項時,優先使用彈出提示。
  • 使用規則與顯示頻率,確保只有正確的受眾能看到每個提示。
  • 當彈出提示已指向圖示時,避免在提示中重複顯示該圖示。

定義提示

Tip 遵循 IdentifiableSendable。至少提供 title;僅在能改善功能探索時刻時,才加入 messageimageactionsrulesoptionsid

import TipKit

struct FavoriteTip: Tip {
    var title: Text { Text("加入最愛") }
    var message: Text? { Text("點擊愛心圖示,將項目保留以便快速存取。") }
    var image: Image? { Image(systemName: "heart.fill") }
}

預設情況下,TipKit 使用提示類型名稱作為 id。對於可重複使用的提示,若其持久化狀態應隨內容而異,請覆寫 id

struct NewItemTip: Tip {
    let itemID: Item.ID

    var id: String { "NewItemTip-\(itemID)" }
    var title: Text { Text("新項目可用") }
}

使用穩定且具體的識別碼。請勿從暫時性的文字或不穩定的排序衍生 ID。

呈現提示

使用 TipView 呈現內嵌提示:

let favoriteTip = FavoriteTip()

VStack {
    TipView(favoriteTip, arrowEdge: .bottom)
    ItemListView()
}

當提示應指向某個控制項時,使用 .popoverTip

Button {
    toggleFavorite()
    favoriteTip.invalidate(reason: .actionPerformed)
} label: {
    Image(systemName: "heart")
}
.popoverTip(favoriteTip, arrowEdge: .top)

規則與事件

規則之間是 AND 關係。僅當所有規則都通過時,提示才符合資格。

使用 @Parameter 處理持久化的應用程式狀態:

struct FavoriteTip: Tip {
    @Parameter static var hasSeenList = false

    var title: Text { Text("加入最愛") }

    var rules: [Rule] {
        #Rule(Self.$hasSeenList) { $0 == true }
    }
}

使用 Tips.Event 處理重複的使用者動作。TipKit 預設查詢最近 1000 次捐贈,因此請保持事件規則的範圍與意圖明確。

struct ShortcutTip: Tip {
    static let manualSaveEvent = Tips.Event(id: "manualSave")

    var title: Text { Text("更快儲存") }

    var rules: [Rule] {
        #Rule(Self.manualSaveEvent) {
            $0.donations.donatedWithin(.week).count >= 3
        }
    }
}

ShortcutTip.manualSaveEvent.sendDonation()

對於更豐富的事件規則,請定義 Tips.Event<DonationInfo>,其中 DonationInfo: Codable, Sendable。保持捐贈資料量小。

當多個提示使用相同事件時,請將相關的事件定義分組到共享的命名空間中;事件 ID 是持久化的邊界,因此衝突可能導致令人困惑的資格判定。

選項與失效

謹慎使用選項;頻率與失效規則是提示持久化行為的一部分。

struct DailyTip: Tip {
    var title: Text { Text("試試篩選器") }

    var options: [any TipOption] {
        MaxDisplayCount(3)
        IgnoresDisplayFrequency(false)
    }
}

MaxDisplayDuration 是 iOS 18+ 的功能。它會累計顯示時間,並且在自動失效發生前有最短連續顯示時間。當應用程式知道所教導的動作或有序步驟已完成時,請勿將其用作明確 invalidate(reason:) 的替代方案。

當使用者執行發現的動作或提示不再相關時,請呼叫 invalidate(reason:)。失效是永久性的,直到資料儲存被重設,或是在 iOS 26+ 上,特定提示呼叫 await resetEligibility() 為止。

favoriteTip.invalidate(reason: .actionPerformed)

使用 .tipClosed 表示明確關閉,僅在描述自動失效結果時使用 .displayCountExceeded.displayDurationExceeded

動作與樣式

當使用者需要直接前往設定、更多資訊或設定流程時,請加入 Action 按鈕。

struct FeatureTip: Tip {
    var title: 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":
        openEditor()
    case "learn-more":
        showHelp()
    default:
        break
    }
}

對於自訂外觀,建議使用 TipViewStyle.Configuration 的值,而不是直接從具體的提示實例讀取。這樣可以保留套用至 TipView 的標籤、處理程式和修飾詞。

struct CompactTipStyle: TipViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        HStack(alignment: .top) {
            configuration.image?
            VStack(alignment: .leading) {
                configuration.title?
                configuration.message?
                ForEach(configuration.actions) { action in
                    Button(action: action.handler) {
                        action.label()
                    }
                }
            }
        }
        .padding()
    }
}

提示群組

TipGroup 是 iOS 18+ 的功能。將群組儲存在 SwiftUI 狀態中,以便可觀察的群組物件在檢視更新之間持續存在。在每次審查 TipGroup(.ordered) 計劃時,請明確區分預設優先順序與有序序列:TipGroup 預設為 .firstAvailable,而當每個後續提示必須等待所有先前提示失效時,才需要使用 TipGroup(.ordered)

struct OnboardingView: View {
    @State private var tips = TipGroup(.ordered) {
        WelcomeTip()
        SearchTip()
        FilterTip()
    }

    var body: some View {
        VStack {
            TipView(tips.currentTip)
            ContentView()
        }
    }
}

MaxDisplayDuration 可以限制顯示時間,但它不是有序群組的排序機制。當同一個群組橫跨多個控制項時,請轉換 currentTip

Button("搜尋") { openSearch() }
    .popoverTip(tips.currentTip as? SearchTip)

測試

僅在除錯/測試程式碼中使用測試覆寫,並在 Tips.configure(_:) 之前套用。

#if DEBUG
if ProcessInfo.processInfo.arguments.contains("--reset-tips") {
    try? Tips.resetDatastore()
}
if ProcessInfo.processInfo.arguments.contains("--show-all-tips") {
    Tips.showAllTipsForTesting()
}
#endif

try Tips.configure()

也提供內建的啟動引數:

  • -com.apple.TipKit.ResetDatastore 1
  • -com.apple.TipKit.ShowAllTips 1
  • -com.apple.TipKit.ShowTips TipTypeA,TipTypeB
  • -com.apple.TipKit.HideAllTips 1

測試覆寫的優先順序為:特定顯示、特定隱藏、全部顯示、全部隱藏。Tips.resetDatastore() 必須在 Tips.configure(_:) 之前執行。

常見錯誤

不要:從檢視中設定 TipKit

請在應用程式初始化時設定。檢視層級的設定可能與提示顯示產生競爭,也可能導致資料儲存已設定的錯誤。

不要:將 iOS 18+ 的 API 當作 iOS 17 的指引

請標記 TipGroup、CloudKit 同步與 MaxDisplayDuration。對於群組優先順序,請套用標準的提示群組決策。

不要:將提示用於關鍵資訊

提示是可關閉且教育性的。對於安全性、錯誤、資料遺失與必要步驟,請使用警示、確認、內嵌警告或阻擋式 UI。

不要:出貨測試覆寫

showAllTipsForTesting() 及相關覆寫會繞過規則與頻率限制。請將其保留在 #if DEBUG、測試方案引數或僅限 UI 測試的啟動引數中。

不要:使用不穩定的可重複使用提示 ID

提示 ID 擁有持久化狀態。如果可重複使用提示的 ID 意外變更,使用者可能會看到重複或過時的教育內容。

審查清單

  • [ ] Tips.configure(_:) 在應用程式初始化時執行一次,且在提示顯示之前。
  • [ ] Tips.resetDatastore() 僅在設定之前執行,且僅用於測試/除錯。
  • [ ] iOS 18+ 與 iOS 26+ 的 TipKit API 具有可用性門檻或備用指引。
  • [ ] 提示文字簡短、情境相關、可操作,且非促銷性質。
  • [ ] 內嵌與彈出呈現方式符合周圍 UI 流程。
  • [ ] 規則針對目標受眾,且不會在首次啟動時顯示所有提示。
  • [ ] 事件 ID 穩定,共享時有命名空間,且捐贈資料量小。
  • [ ] 可重複使用的提示使用穩定的內容衍生值覆寫 id
  • [ ] 當使用者執行所教導的動作時,提示會失效。
  • [ ] TipGroup 保持在 @State 中,並遵循提示群組的優先順序決策。
  • [ ] CloudKit 同步使用 iCloud + CloudKit、遠端通知,並在適當時使用專用容器。
  • [ ] 自訂樣式使用 configuration 值並呼叫 action.label()
  • [ ] 測試覆寫僅限除錯/測試,且絕不會在正式環境中啟用。

參考資料