TipKit
iOS 17+アプリに機能発見のヒント、コンテキストヒント、オンボーディングのコーチマークを追加します。AppleのTipKitフレームワークを使用します。TipKitは表示頻度、表示条件、永続化を管理するため、ヒントは適切なタイミングで表示され、ユーザーが機能を学習すると非表示になります。
目次
セットアップ
Tips.configure()をApp.init内で、ビューがレンダリングされる前に一度だけ呼び出します。これにより、tipデータストアが初期化され、ルールの評価が開始されます。後で呼び出すと、tipビューがデータストアの準備ができる前に表示を試みる競合状態が発生するリスクがあります。
import SwiftUI
import TipKit
@main
struct MyApp: App {
init() {
try? Tips.configure([
.datastoreLocation(.applicationDefault)
])
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
DatastoreLocationオプション
| オプション | ユースケース |
|---|---|
.applicationDefault | デフォルトの場所、アプリサンドボックス(ほとんどのアプリ) |
.groupContainer(identifier:) | アプリと拡張機能間でtipの状態を共有 |
.url(_:) | ストレージ場所を完全に制御するためのカスタムファイルURL |
CloudKit同期
ユーザーのデバイス間でtipの状態を同期し、すべてのデバイスで同じtipが表示されないようにします。データストアの場所と一緒にCloudKitコンテナオプションを追加します。
try? Tips.configure([
.datastoreLocation(.applicationDefault),
.cloudKitContainer(.named("iCloud.com.example.app"))
])
Tipの定義
Tipプロトコルに準拠した構造体を定義します。最低でもtitleを提供します。サポートする詳細情報にはmessage、先頭のアイコンにはimageを追加します。タイトルは短くアクション指向にします。これは、tipがコンパクトなコールアウトとして表示されるためです。
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(表示頻度、最大表示回数)
ライフサイクル: 保留中(ルール未満) -> 表示可能(すべてのルール通過) -> 無効化(却下、アクション実行、またはプログラムによる削除)。一度無効化されると、データストアがリセットされない限り、tipは再表示されません。
Tipの表示
インラインTip(TipView)
TipViewをレイアウトに直接埋め込みます。アニメーション付きで表示・非表示になる角丸のカードとしてレンダリングされます。スクロール可能なコンテンツ内のtipに使用します。
let favoriteTip = FavoriteTip()
var body: some View {
VStack {
TipView(favoriteTip)
ItemListView()
}
}
ポップオーバーTip(.popoverTip())
任意のビューにアンカーとしてtipをポップオーバーで表示します。フレームワークはポップオーバーからアンカーに向かって矢印を描画します。特定のコントロールを指すtipに使用します。
Button { toggleFavorite() } label: { Image(systemName: "heart") }
.popoverTip(favoriteTip)
// 矢印の方向を制御(省略するとシステムが選択)
.popoverTip(favoriteTip, arrowEdge: .bottom)
カスタムTipViewStyle
カスタムスタイルを作成して、アプリ全体のtipの外観を制御します。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())
Tipルール
ルールは、いつtipが表示可能になるかを制御します。rules配列内のすべてのルールが満たされる必要があります。TipKitは、パラメータベースとイベントベースの2種類のルールをサポートしています。
パラメータベースルール
@Parameterを使用してアプリの状態を追跡します。パラメータ値がルール条件を満たすと、tipが表示可能になります。
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を使用してユーザーアクションを追跡します。アクションが発生するたびにイベントに寄付(donate)します。寄付回数またはタイミング条件が満たされると、ルールが発火します。これは、ユーザーが特定のアクションを数回実行しても関連機能に気づかない場合に、ヒントを表示するのに理想的です。
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プロパティを使用して、tipが表示される頻度を制御します。
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の場合、特定のtipがIgnoresDisplayFrequency(true)を設定していない限り、システムはアプリ全体で1日あたり最大1つのtipを表示します。
Tipアクション
tipにアクションボタンを追加して、直接操作できるようにします。各アクションにはidとラベルがあります。tipビューのアクションハンドラでアクションを処理します。
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
}
}
Tipグループ
TipGroupを使用して、単一のビュー内で複数のtipを調整します。TipGroupは、グループから一度に1つのtipのみが表示されるようにし、tipの過剰表示を防ぎます。tipは優先順位に従って表示されます。
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 | tipはリストされた順序で表示されます |
現在のtipが無効化されると、グループ内の次の表示可能なtipがcurrentTipになります。
プログラムによる制御
Tipの無効化
ユーザーが発見したアクションを実行した場合、またはtipが不要になった場合に、invalidate(reason:)を呼び出します。
let tip = FavoriteTip()
tip.invalidate(reason: .actionPerformed)
| 理由 | 使用タイミング |
|---|---|
.actionPerformed | ユーザーがtipで説明されているアクションを実行した |
.displayCountExceeded | tipが最大表示回数に達した |
.tipClosed | ユーザーが明示的にtipを閉じた |
テスト用ユーティリティ
TipKitには、開発中およびテスト中にtipの表示を制御するための静的メソッドが用意されています。これらは#if DEBUGまたはProcessInfoチェックの背後にゲートして、本番ビルドで実行されないようにします。
#if DEBUG
// ルールに関係なくすべてのtipを表示(開発中に便利)
Tips.showAllTipsForTesting()
// 特定のtipのみを表示
Tips.showTipsForTesting([FavoriteTip.self, ShortcutTip.self])
// すべてのtipを非表示(tipを含まないUIテストに便利)
Tips.hideAllTipsForTesting()
// データストアをリセット(すべてのtipの状態、無効化、イベントをクリア)
try? Tips.resetDatastore()
#endif
テストスキームでのProcessInfoの使用
if ProcessInfo.processInfo.arguments.contains("--show-all-tips") {
Tips.showAllTipsForTesting()
}
Xcodeスキームの起動引数として--show-all-tipsを渡します(開発ビルド用)。
よくあるミス
してはいけないこと: Tips.configure()をApp.init以外で呼び出す
ビューのonAppearまたはtask修飾子でTips.configure()を呼び出すと、tipビューがデータストアの準備ができる前にレンダリングしようとする競合状態が発生し、tipが欠落したりちらついたりします。
// 間違い
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() } }
}
してはいけないこと: 一度に多くのtipを表示する
複数のtipを同時に表示すると、ユーザーを圧倒し、各tipの効果が薄れます。ユーザーはそれらを無視するようになります。
// 間違い: 3つのtipが同時に表示される
VStack {
TipView(tipA)
TipView(tipB)
TipView(tipC)
}
// 正しい: TipGroupを使用して順序付ける
let group = TipGroup(.ordered) { TipA(); TipB(); TipC() }
if let currentTip = group.currentTip {
TipView(currentTip)
}
してはいけないこと: ユーザーがアクションを実行した後にtipを無効化するのを忘れる
tipに「星をタップしてお気に入りに追加」と表示され、ユーザーが星をタップしたのにtipが残っている場合、UIへの信頼が損なわれます。
// 間違い: ユーザーがアクションを実行した後もtipが表示されたまま
Button("お気に入り") { toggleFavorite() }
.popoverTip(favoriteTip)
// 正しい: アクション時に無効化
Button("お気に入り") {
toggleFavorite()
favoriteTip.invalidate(reason: .actionPerformed)
}
.popoverTip(favoriteTip)
してはいけないこと: テスト用tipを本番環境で有効にしたままにする
Tips.showAllTipsForTesting()はすべてのルールと頻度制限をバイパスします。これを本番環境に出荷すると、すべてのユーザーがすべてのtipをすぐに目にすることになります。
// 間違い: 常に有効
Tips.showAllTipsForTesting()
// 正しい: DEBUGの後ろにゲート
#if DEBUG
Tips.showAllTipsForTesting()
#endif
してはいけないこと: tipのタイトルを長くしすぎる
長いタイトルは、コンパクトなtipコールアウト内で切り詰められたり、不格好に折り返されたりします。主要なアクションはタイトルに、補足的なコンテキストはメッセージに入れます。
// 間違い
var title: Text { Text("ハートボタンをタップして、このアイテムをお気に入りリストに保存できます") }
// 正しい
var title: Text { Text("お気に入りに保存") }
var message: Text? { Text("ハートアイコンをタップして、アイテムをすぐにアクセスできるように保存します。") }
してはいけないこと: 重要な情報にtipを使用する
ユーザーはいつでもtipを閉じることができ、再表示されません。重要な指示や安全情報をtipに入れてはいけません。
// 間違い: 閉じられるtipに重要な情報
struct DataLossTip: Tip {
var title: Text { Text("保存されていない変更は失われます") }
}
// 正しい: 重要な情報にはアラートまたはインライン警告を使用する
// tipは機能発見とプログレッシブディスクロージャーに限定する
レビューチェックリスト
-
Tips.configure()がApp.initで、すべてのビューがレンダリングされる前に呼び出されている - 各tipに明確で簡潔なタイトルがある(アクション指向、約40文字以内)
- ユーザーが発見したアクションを実行したときにtipが無効化される
- ルールが適切に設定され、tipが適切なタイミングで表示される(すべてのtipが初回起動時にすぐに表示されるわけではない)
- 1つのビューに複数のtipがある場合、
TipGroupが使用されている - テスト用ユーティリティ(
showAllTipsForTesting、resetDatastore)が#if DEBUGの後ろにゲートされている - アプリが複数のデバイスをサポートする場合、CloudKit同期が設定されている
- 表示頻度が適切に設定されている(ほとんどのアプリでは
.dailyまたは.weekly) - tipは機能発見にのみ使用され、重要な情報には使用されていない
- デフォルトのスタイルがアプリのデザインに合わない場合、カスタム
TipViewStyleが一貫して適用されている - tipアクションが処理され、アクションハンドラでtipが無効化されている
- イベントへの寄付が正しいユーザーアクションポイントに配置されている
- カスタムTip型がSendableであることを確認し、Tipsは@MainActor上で設定する
参考資料
- 完全な実装パターン(カスタムスタイル、イベントベースルール、tipグループ、テスト戦略、オンボーディングフロー、SwiftUIプレビュー設定を含む)については、references/tipkit-patterns.mdを参照してください。