mcpField
T
tipkit

tipkit

github.com/dpearson2699设计与 UI1.2k3.6k 次安装13 周前更新
代码仓库

关于 tipkit

面向 Apple 平台应用开发者的 TipKit 技能,用于添加情境化的功能发现提示。内容涵盖呈现方式、资格规则、状态保存、测试与 API 可用条件,帮助控制提示的简短程度和触发时机。

主要功能

  1. 01内联和弹出式提示呈现
  2. 02资格规则与事件记录
  3. 03显示次数限制与提示失效
  4. 04提示分组与顺序展示
  5. 05CloudKit 和 App Group 状态配置
  6. 06测试覆盖与 API 可用性指导

适用场景

  1. 01在新功能附近说明其操作方式
  2. 02仅在指定用户操作后显示提示
  3. 03按顺序呈现多个功能发现提示
  4. 04在调试版本中验证提示显示条件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill tipkit

安装后在 Claude Code 中输入/tipkit确认已加载。

附带文件evals/evals.jsonreferences/tipkit-patterns.md

详细介绍

TipKit

使用 TipKit 展示小巧、契合情境的功能发现提示:内嵌提示、弹出式提示、受规则控制的引导,以及轻量的操作指引标记。通用的 SwiftUI 架构、导航、布局和较长的首次运行引导流程,请交由相关技能处理,除非 TipKit 的展示方式本身是核心问题。

目录

可用性

TipKit 的核心功能 Tip、TipView、popoverTip、规则、事件、选项和测试覆盖项,适用于 iOS 17+、iPadOS 17+、macOS 14+、tvOS 17+、watchOS 10+ 和 visionOS 1+。

明确限制较新 API 的使用范围:

API可用性用途
TipGroupiOS 18+对提示进行分组或排序;请参照提示组中的决策。
.cloudKitContainer(...)iOS 18+在设备间同步提示状态、参数、事件和展示次数。
MaxDisplayDurationiOS 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 configuration failed: \(error)")
        }
    }

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

仅当应用与扩展或应用组成员需要有意共享提示状态时,才使用 .datastoreLocation(.groupContainer(identifier:))。请确保应用组成员的选项设置一致,因为 TipKit 会将选项状态与提示记录一同持久化。

CloudKit 同步

仅在 iOS 18+ 及更高版本中使用 CloudKit 同步。启用 iCloud + CloudKit 和 Background Modes > Remote notifications,然后传入容器:

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

建议使用带有 .tips 后缀的专用容器。如果存在已授权的 .tips 容器,.automatic 会优先使用该容器,否则回退到主容器。

设计优质提示

提示是简短、临时的帮助信息。用于介绍人们只需几个简单步骤就能理解并尝试的功能。如果流程需要长篇解释、多个屏幕,或涉及关键安全/错误信息,请改用教程、警告框、内嵌警告或引导流程。

遵循与 HIG 一致的默认做法:

  • 标题简短、直接,并以行动为导向。
  • 使用一到两句话;避免宣传性或无关的文案。
  • 将提示放在所说明功能的附近。
  • 如果隐藏附近的 UI 会打断任务,优先使用内嵌提示。
  • 如果保持当前布局很重要,且提示可以指向特定控件,优先使用弹出式提示。
  • 使用规则和展示频率,确保只有合适的用户会看到各条提示。
  • 如果弹出式提示已经指向某个图标,就避免在提示中重复该图标。

定义提示

Tip 遵循 Identifiable 和 Sendable。至少提供 title;仅在能改善功能发现体验时,才添加 message、image、actions、rules、options 和 id。

import TipKit

struct FavoriteTip: Tip {
    var title: Text { Text("Save to Favorites") }
    var message: Text? { Text("Tap the heart to keep items for quick access.") }
    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("New Item Available") }
}

使用稳定、明确的标识符。不要根据临时文案或不稳定的排序生成 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("Save to Favorites") }

    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("Save Faster") }

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

ShortcutTip.manualSaveEvent.sendDonation()

若要定义更丰富的事件规则,可定义 Tips.Event<DonationInfo>,其中 DonationInfo: Codable, Sendable。捐赠载荷应保持精简。

当多个提示使用相同事件时,将相关事件定义归入共享命名空间;事件 ID 是持久化边界,因此 ID 冲突可能导致符合显示条件的情况难以理解。

选项与失效

谨慎使用选项;显示频率和失效规则都是提示持久化行为的一部分。

struct DailyTip: Tip {
    var title: Text { Text("Try Filters") }

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

MaxDisplayDuration 适用于 iOS 18+。它会累计显示时间,并设定自动失效前的最短连续显示时长。当应用知道用户已完成所教的操作或有序步骤时,不要用它代替显式调用 invalidate(reason:)。

当用户执行已发现的操作,或提示不再相关时,调用 invalidate(reason:)。失效是永久性的,直到重置 datastore,或者在 iOS 26+ 中由特定提示调用 await resetEligibility()。

favoriteTip.invalidate(reason: .actionPerformed)

显式关闭时使用 .tipClosed;只有在描述自动失效的结果时,才使用 .displayCountExceeded 或 .displayDurationExceeded。

操作与样式

当用户需要直接前往设置、了解更多信息或进入设置流程时,添加 Action 按钮。

struct FeatureTip: Tip {
    var title: Text { Text("Try the New Editor") }

    var actions: [Action] {
        Action(id: "open-editor", title: "Open Editor")
        Action(id: "learn-more", title: "Learn More")
    }
}

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("Search") { 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.configure(_:) 之前运行 Tips.resetDatastore()。

常见错误

不要:从视图中配置 TipKit

在应用初始化期间进行配置。视图级别的配置可能与提示显示发生竞争,也可能触发 datastore 已配置错误。

不要:将 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 中,并遵循 Tip Groups 优先级决策。
  • CloudKit 同步使用 iCloud + CloudKit、Remote notifications,并在适当情况下使用专用容器。
  • 自定义样式使用 configuration 值,并调用 action.label()。
  • 测试覆盖仅用于调试/测试,绝不在生产环境中启用。

参考资料

评论

支持 Markdown