tipkit

tipkit

热门

实现并审查 Apple TipKit 功能发现 UI,适用于 iOS 17+ 应用。在添加或审核应用内提示、上下文帮助、教练标记、Tip、TipView、popoverTip、规则、事件、操作、显示频率、测试覆盖、可重用提示标识符或 iOS 18+ 的 TipGroup 和 CloudKit 提示同步时使用;避免用于通用 SwiftUI 导航或提示展示之外的布局。

936Star
47Fork
更新于 2026/7/15
SKILL.md
readonly只读
name
tipkit
description

实现并审查 Apple TipKit 功能发现 UI,适用于 iOS 17+ 应用。在添加或审核应用内提示、上下文帮助、教练标记、Tip、TipView、popoverTip、规则、事件、操作、显示频率、测试覆盖、可重用提示标识符或 iOS 18+ 的 TipGroup 和 CloudKit 提示同步时使用;避免用于通用 SwiftUI 导航或提示展示之外的布局。

TipKit

使用 TipKit 实现小型、上下文相关的功能发现时刻:内联提示、弹出提示、规则控制的教育和轻量级教练标记。将通用的 SwiftUI 架构、导航、布局和较长的首次运行引导流程保留在兄弟技能中,除非 TipKit 展示是核心问题。

目录

可用性

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()
  • [ ] 测试覆盖仅用于调试/测试,并且永远不会在生产中激活。

参考