WWDC Quick Look 💓 By SwiftGGTeam
Make features discoverable with TipKit

Make features discoverable with TipKit

观看原视频

Highlight

TipKit 让 App 功能提示的创建、展示控制和精准投放变成几行代码的事,支持参数规则和事件规则组合定位目标用户,避免教育信息变成骚扰弹窗。

核心内容

App 里做了好功能,用户却找不到——这是每个开发者都遇到过的困境。传统的做法是在首次启动时做长篇大论的引导页,用户要么快速跳过,要么看完就忘。

TipKit 的思路不同:在正确的时机、向正确的人、展示一句恰到好处的提示。它覆盖 iPhone、iPad、Mac、Apple Watch 和 Apple TV,一套 API 全平台通用。

创建提示

提示由标题和消息组成。标题用直接的动作短语,比如”收藏你喜欢的后院”。消息说明收益,比如”收藏的后院会显示在列表顶部”。(01:53

提示可以附加图标(SF Symbol)和 Action 按钮。图标帮助用户建立视觉关联,Action 按钮可以跳转到设置页面或引导流程。

但不是所有场景都适合用 TipKit。广告推销、错误消息、功能更新通知、复杂的多步操作指南——这些都不属于用户教育范畴。(02:25

两种展示方式

Popover:浮层形式,指向特定 UI 元素,不改变当前页面布局。适合引导用户点击某个按钮。tvOS 只支持这种方式。(04:15

Inline:内嵌形式,App UI 自动让出空间,不会遮挡内容。适合在列表或表单中展示提示。

规则系统

规则是 TipKit 的核心能力,确保提示只展示给最需要的人。(06:02

参数规则(Parameter-based):基于 Swift 值类型的持久状态。比如用户是否已登录、是否开启了某个设置。

事件规则(Event-based):基于用户行为计数。比如”访问详情页 3 次后才展示提示”。事件可以带时间范围(“过去 5 天内访问 3 次”)和自定义关联类型(“访问同一个后院 3 次”)。

规则可以组合使用。比如”已登录 + 过去 5 天访问详情页 3 次 + 从未收藏过后院”——三个条件同时满足才展示提示。

展示控制

多个提示同时存在时,需要控制展示节奏避免 overwhelm 用户。(09:57

TipsCenter 提供全局的展示频率控制:

  • .daily:每天最多展示一个提示
  • .hourly:每小时最多展示一个
  • .immediate:满足条件立即展示,不做频率限制
  • 自定义 TimeInterval:比如每 5 天一个

单个提示可以设置 .ignoresDisplayFrequency(true) 跳过全局频率限制,适合特别重要的功能引导。也可以设置 .maxDisplayCount(5) 限制总展示次数,超过后自动失效。

提示的消除也有几种方式:用户执行了提示描述的操作(调用 invalidate(reason: .userPerformedAction))、达到最大展示次数、或者开发者根据任意条件调用 invalidate()。(11:27

测试

TipKit 提供了完整的测试 API,不需要满足规则就能看到提示效果:(12:46

  • TipsCenter.showAllTips():展示所有提示
  • TipsCenter.showTips([tip1, tip2]):只展示指定提示
  • TipsCenter.hideAllTips():隐藏所有提示
  • TipsCenter.resetDatastore():清空所有 TipKit 数据

这些也可以通过 Xcode Scheme 的 Launch Arguments 配置,方便快速测试。

详细内容

基础提示定义

import TipKit

struct FavoriteBackyardTip: Tip {
    var title: Text {
        Text("Save as a Favorite")
    }
    
    var message: Text {
        Text("Your favorite backyards always appear at the top of the list.")
    }
}

关键点:

  • 遵守 Tip 协议,至少实现 titlemessage
  • titlemessage 返回 Text 类型,支持 SwiftUI 修饰符
  • 提示定义是值类型,可以全局创建实例复用

带图标和 Action 的提示

struct FavoriteBackyardTip: Tip {
    var title: Text {
        Text("Save as a Favorite")
            .foregroundColor(.indigo)
    }
    
    var message: Text {
        Text("Your favorite backyards always appear at the top of the list.")
    }
    
    var asset: Image {
        Image(systemName: "star")
    }
    
    var actions: [Action] {
        [
            Tip.Action(
                id: "learn-more",
                title: "Learn More"
            )
        ]
    }
}

关键点:

  • asset 用 SF Symbol 或自定义图片,帮助用户建立视觉关联
  • actions 数组定义按钮,每个 Action 有唯一 ID 和标题
  • .popoverMiniTip 的回调中通过 action ID 处理点击

初始化配置

@main
struct BackyardBirdsApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

    init() {
        TipsCenter.shared.configure()
    }
}

关键点:

  • TipsCenter.shared.configure() 必须在 App 启动时调用
  • 它负责提示状态和事件数据的持久化
  • 不调用的话,事件计数和提示展示状态不会在 App 重启后保留

Popover 展示

private let favoriteBackyardTip = FavoriteBackyardTip()

// ...

.toolbar {
    ToolbarItem {
        Button {
            backyard.isFavorite.toggle()
        } label: {
            Label("Favorite", systemImage: "star")
                .symbolVariant(
                    backyard.isFavorite ? .fill : .none
                )
        }
        .popoverMiniTip(tip: favoriteBackyardTip)
    }
}

关键点:

  • .popoverMiniTip(tip:) 是 SwiftUI 修饰符,自动把提示浮层附加到目标视图
  • 提示会自动指向修饰符所在的视图元素
  • 提示的显示/隐藏由 TipKit 根据规则自动管理,不需要手动控制

参数规则

struct FavoriteBackyardTip: Tip {
    @Parameter
    static var isLoggedIn: Bool = false

    var rules: Predicate<RuleInput...> {
        #Rule(Self.$isLoggedIn) { $0 == true }
    }
}

关键点:

  • @Parameter 标记的静态属性是持久化的,App 重启后值保留
  • 在登录成功后设置 FavoriteBackyardTip.isLoggedIn = true
  • #Rule 宏定义条件,支持比较运算符和逻辑组合

事件规则

struct FavoriteBackyardTip: Tip {
    @Parameter
    static var isLoggedIn: Bool = false
    
    static let enteredBackyardDetailView: Event = Event<DetailViewDonation>(
        id: "entered-backyard-detail-view"
    )

    var rules: Predicate<RuleInput...> {
        #Rule(Self.$isLoggedIn) { $0 == true }
        #Rule(Self.enteredBackyardDetailView) { $0.count >= 3 }
    }
}

// 在视图中捐赠事件
.onAppear {
    FavoriteBackyardTip.enteredBackyardDetailView.donate()
}

关键点:

  • Event 定义需要唯一 ID,可以带关联类型 DonationValue
  • .donate() 在事件发生时调用,TipKit 自动记录时间戳
  • $0.count 返回该事件的总捐赠次数
  • 多个 #Rule 默认是 AND 关系,必须全部满足

带时间过滤的事件规则

#Rule(Self.enteredBackyardDetailView) {
    $0.donations.filter {
        $0.date > Date.now.addingTimeInterval(-5 * 60 * 60 * 24)
    }
    .count >= 3
}

关键点:

  • $0.donations 返回所有捐赠记录,包含 date 和关联数据
  • filter 筛选时间范围内的记录
  • 时间计算用 addingTimeInterval,单位是秒

带关联类型的事件规则

extension FavoriteBackyardTip {
    struct DetailViewDonation: DonationValue {
        let backyardID: Int
    }
}

// 捐赠时附带数据
.onAppear {
    FavoriteBackyardTip.enteredBackyardDetailView.donate(
        with: .init(backyardID: backyard.id)
    )
}

// 规则中按 backyardID 分组统计
#Rule(Self.enteredBackyardDetailView) {
    $0.donations.filter {
        $0.date > Date.now.addingTimeInterval(-5 * 60 * 60 * 24)
    }
    .largestSubset(by: \.backyardID)
    .count >= 3
}

关键点:

  • 关联类型遵守 DonationValue 协议,可以存任意可序列化数据
  • .largestSubset(by:) 按指定属性分组,返回最大组的记录数
  • 这个例子表示”过去 5 天内,访问同一个后院详情页至少 3 次”
  • 关联数据越小查询越快,建议只存 ID 不要存完整对象

展示频率配置

// 全局频率:每天一个提示
TipsCenter.shared.configure {
    DisplayFrequency(.daily)
}

// 单个提示跳过频率限制
struct FavoriteBackyardTip: Tip {
    var options: [Option] {
        [.ignoresDisplayFrequency(true)]
    }
}

// 单个提示最多展示 5 次
struct FavoriteBackyardTip: Tip {
    var options: [Option] {
        [.maxDisplayCount(5)]
    }
}

关键点:

  • 全局频率在 TipsCenter 级别配置,影响所有提示
  • .ignoresDisplayFrequency 让单个提示不受全局限制
  • .maxDisplayCount 限制提示的总展示次数,达到后自动失效

提示失效

Button {
    backyard.isFavorite.toggle()
    favoriteBackyardTip.invalidate(reason: .userPerformedAction)
} label: {
    Label("Favorite", systemImage: "star")
        .symbolVariant(backyard.isFavorite ? .fill : .none
}
.popoverMiniTip(tip: favoriteBackyardTip)

关键点:

  • .userPerformedAction 表示用户执行了提示描述的操作
  • 调用 invalidate() 后提示立即消失,且不会再展示
  • 也可以用 .tipClosed 表示用户手动关闭,或自定义原因

核心启发

1. 渐进式功能引导系统 做什么:为 App 的核心功能设计一套渐进式提示体系,新用户按顺序发现功能,而不是一次性塞给所有信息。 为什么值得做:TipKit 的规则系统让”在正确时机展示正确提示”变得可编程,用户体验比传统引导页好得多。 怎么开始:列出 App 的 5-10 个关键功能,为每个功能创建 Tip,设置事件规则(如”使用功能 A 3 次后提示功能 B”)。

2. 智能用户分层 做什么:根据用户行为数据,用事件规则自动区分新手、普通用户和高级用户,展示不同的提示。 为什么值得做:同一个提示对新手有用,对老用户就是骚扰。TipKit 的自定义捐赠类型让精细分层成为可能。 怎么开始:定义用户行为事件(如”创建第一个项目”、“使用高级功能 X”),用关联类型记录行为详情,在规则中组合判断用户阶段。

3. 跨设备提示同步 做什么:利用 TipKit 的 iCloud 同步能力,确保用户在 iPhone 上看过的提示不会在 iPad 上重复展示。 为什么值得做:多设备用户越来越多,重复教育会降低产品质感。 怎么开始:TipKit 默认开启 iCloud 同步,无需额外代码。只需要确保各平台的提示 ID 一致。

4. A/B 测试提示文案 做什么:为同一个功能准备多组提示文案,通过事件规则控制展示比例,用转化率数据验证哪组效果更好。 为什么值得做:提示的标题和消息直接影响用户是否愿意尝试新功能,文案优化有明确的 ROI。 怎么开始:创建两个 Tip 类型(如 FavoriteTipVariantAFavoriteTipVariantB),用参数规则控制展示比例,通过 Action 按钮的点击率和功能使用率评估效果。

关联 Session

评论

GitHub Issues · utterances