ハイライト
TipKit は iOS 17 で「Tip をどう表示するか」を解決しましたが、実際に使うと複数 Tip が同時に出たり、同じ Tip 構造を別コンテンツに再利用できなかったり、デバイスを変えると Tip 状態がリセットされたりします。今年の更新はこれらの課題に直接応えます。
主要内容
app に 3 つの新機能を追加し、それぞれに Tip を付けました。ユーザーが初めて app を開くと 3 つの Tip が同時にポップアップし、互いに重なって——ユーザーはすべて閉じ、オンボーディング戦略は即座に失敗します。これが TipKit で昨年最も批判されたシナリオです。複数 Tip に制御能力がなく、順序の保証がありませんでした。
今年 Apple は TipKit に TipGroup を追加しました。関連 Tip を同じ Group に入れ、.ordered 優先順位を指定し、前の Tip が invalidate されてから次を表示できます。.firstAvailable なら、表示条件を満たした Tip から出します。同時に表示される Tip は最大 1 つになり、体験がすぐに制御可能になります。
ただし TipGroup は「順序」問題だけを解決します。もう 1 つのよくある課題:10 件の新コンテンツ(例:10 本の新ルート)があり、各コンテンツに Tip を 1 回表示したい——10 個の Tip 構造体を書く必要があるでしょうか?カスタム Identifier がこれを解決します。1 つの Tip 構造体で id プロパティをオーバーライドし、具体コンテンツの識別子にバインドすれば、同じ構造体を繰り返しインスタンス化でき、各コンテンツに独立した表示状態を持てます。TipViewStyle で Tip の見た目を完全に制御し、CloudKit 同期でデバイス間の状態を一致させる——4 つの能力が重なり、TipKit は「表示できる」から「制御できる」へ進化しました。
詳細
TipGroup:複数 Tip の表示順序を制御
同じコントロールに 2 つの popoverTip を付けると同時に出る可能性があります。TipGroup に書き換えると、現在の Tip が invalidate されてから次が表示されます(02:41):
struct MapCompassControl: View {
@State
var compassTips: TipGroup(.ordered) {
ShowLocationTip()
RotateMapTip()
}
var body: some View {
CompassDial()
.popoverTip(compassTips.currentTip)
.onTapGesture {
showCurrentLocation()
}
.onLongPressGesture(minimumDuration: 0.1) {
reorientMapHeading()
}
}
}
キーポイント:
TipGroup(.ordered)は宣言順に表示。.firstAvailableはルールを満たした最初の Tip を表示compassTips.currentTipは Group 内で現在表示すべき Tip を返します.orderedモードでは、前の Tip が invalidate されないと次は表示されません- 異なる Tip を異なるコントロールに付ける場合、
currentTip as? ShowLocationTipで型キャスト(03:15) - ユーザーが実際に機能を使った後、
invalidate(reason: .actionPerformed)を手動呼び出し。Group が自動的に次へ進みます(03:50)
カスタム Identifier:同じ Tip 構造体を別コンテンツに再利用
デフォルトでは Tip の識別子は型名です。つまり 1 Tip タイプに 1 表示記録しかありません。動的コンテンツ(新記事、新ルート、新機能項目)では各項目に独立した表示状態が必要です。id プロパティをオーバーライドします(06:45):
struct NewTrailTip: Tip {
let newTrail: Trail
var title: Text {
Text("\(newTrail.name) is now available")
}
var message: Text? {
Text("To see key trail info, tap \(newTrail.region) on the map.")
}
var actions: [Action] {
Action(title: "Go there now")
}
var id: String {
"NewTrailTip-\(newTrail.id)"
}
var rules: [Rule] {
#Rule(newTrail.region.didVisitEvent) {
$0.donations.count > 3
}
}
}
キーポイント:
idはnewTrail.idから構築。各ルートに独立した識別子- 識別子が異なれば表示状態も独立。ルート A で dismiss されてもルート B には表示可能
rulesもnewTrail.regionにバインドし、各 Tip の表示条件をコンテンツに紐づけ- ID は安定した具体値(ユーザー ID、コンテンツ ID)に基づくべき。変動しやすい値は使わない
TipViewStyle:Tip の外観を完全カスタマイズ
デフォルト Tip スタイルはシステム標準カードです。ブランドビジュアル要件がある場合、TipViewStyle でレイアウトをカスタマイズ(08:55):
struct NewTrailTipViewStyle: TipViewStyle {
func makeBody(configuration: Configuration) -> some View {
let tip = configuration.tip as! NewTrailTip
let highlightTrailAction = configuration.actions.first!
TrailImage(imageName: tip.newTrail.heroImage)
.frame(maxHeight: 150)
.onTapGesture { highlightTrailAction.handler() }
.overlay {
VStack {
configuration.title.font(.title)
HStack {
configuration.message.font(.subheadline)
Spacer()
Image(systemName: "chevron.forward.circle")
.foregroundStyle(.white)
}
}
}
}
}
キーポイント:
configuration.tipを具体 Tip 型にキャストし、カスタムプロパティ(newTrail.heroImageなど)にアクセスtip.titleではなくconfiguration.titleとconfiguration.messageを使い、TipView に外部追加した modifier が効くようにするconfiguration.actionsで Tip 定義の Action を取得。handler()で TipView クロージャ内のコールバックをトリガー- スタイル適用:
.tipViewStyle(NewTrailTipViewStyle())(09:20) - UIKit/AppKit app は
viewStyleプロパティで設定可能
CloudKit 同期:Tip 状態をデバイス間で共有
ユーザーが iPhone で Tip を閉じ、iPad に移ると同じ Tip が再表示——この体験は非常に悪いです。CloudKit 同期を設定すると、Tip の表示状態、イベント donation、表示回数がデバイス間で同期されます(11:38):
@main
struct TipKitTrails: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
await configureTips()
}
}
}
func configureTips() async {
do {
try Tips.configure([
.cloudKitContainer(.named("iCloud.com.apple.TipKitTrails.tips")),
.displayFrequency(.weekly)
])
}
catch {
print("Unable to configure tips: \(error)")
}
}
}
キーポイント:
- Xcode プロジェクトで iCloud 能力を有効化し、CloudKit サービスを追加してコンテナを作成
- Background Modes の Remote Notifications も有効化し、TipKit がバックグラウンドでリモート変更を処理
Tips.configure()に.cloudKitContainer(.named(...))を渡せば同期有効- 同期内容:Tip 表示状態、イベントとパラメータ値、表示回数と継続時間
- プラットフォームごとに独立表示が必要な Tip(iPhone と iPad で各 1 回)は
UIDeviceでプラットフォーム関連 ID を構築 - テスト時
resetDatastore()はローカルデータと CloudKit レコードの両方をクリア
重要ポイント
-
何を作るか:TipGroup で機能ガイドフローを制御します。初回ガイド Tip を
.orderedでグループ化し、ユーザーが 1 ステップ完了してから次を表示。情報過多を避けます。なぜ価値があるか:複数 Tip を同時に見るとユーザーはほぼすべて閉じます。1 つずつ表示すれば完了率が大きく向上。どう始めるか:同じ画面に複数popoverTipがある箇所を見つけ、TipGroup(.ordered)+currentTipに変更。 -
何を作るか:カスタム Identifier でコンテンツ駆動 Tip を再利用可能に。EC の新商品、読書 app の新記事、地図 app の新ルート——「同じ構造、異なるコンテンツ」の Tip シナリオすべてに
idオーバーライドで 1 Tip 構造体の再利用が適しています。なぜ価値があるか:各新コンテンツごとに独立 Tip 型を書く必要がなく、コード量と保守コストが大幅削減。どう始めるか:コンテンツ種別ごとに独立 Tip を書いているモジュールを 1 つ選び、let content: ContentTypeとvar id: String { "Tip-\(content.id)" }を持つ汎用 Tip に統合。 -
何を作るか:TipViewStyle で Tip をブランド化コンテンツカードに。Tip がリッチコンテンツ(画像、リンク、クイック操作)を担う場合、デフォルトスタイルでは足りず、カスタム Style でインタラクティブなコンテンツ推薦カードにできます。なぜ価値があるか:Tip はシステム通知ボックスに過ぎず、app コンテンツ配信の一部になり、ガイドとコンテンツ体験が一体化。どう始めるか:
TipViewStyleプロトコルのmakeBodyを実装し、configuration.tipでカスタムプロパティを取得してレイアウト、configuration.actionsで操作を処理。 -
何を作るか:マルチデバイス app では初日から CloudKit 同期を有効化。Tip 状態のデバイス間不一致はユーザーから最もよく聞く不満の 1 つ。後から同期を追加すると既存ローカル状態との競合処理が必要。なぜ価値があるか:Day 1 から同期すれば、iPhone で閉じた Tip を iPad で再表示されず、イベント donation もデバイス間で累積しルール判定がより正確。どう始めるか:Xcode プロジェクトに iCloud + CloudKit コンテナ + Background Modes Remote Notifications を追加し、
Tips.configureに.cloudKitContainerオプションを追加。
関連セッション
- Make features discoverable with TipKit — 昨年の TipKit 入門 Session。Tip 作成、表示ルール設定、頻度制御
- What’s new in SwiftData — TipKit の永続化は SwiftData ベース。SwiftData の新機能理解が TipKit ストレージ理解に役立つ
- Use CloudKit Console to monitor and optimize database activity — CloudKit 同期有効化後、Console で DB アクティビティを監視・最適化
- Bring your app’s core features to users with App Intents — App Intents と TipKit はともに機能発見に貢献。前者はシステムレベル入口、後者は app 内ガイド
コメント
GitHub Issues · utterances