WWDC Quick Look 💓 By SwiftGGTeam
Customize feature discovery with TipKit

Customize feature discovery with TipKit

元の動画を見る

ハイライト

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
        }
    }
}

キーポイント:

  • idnewTrail.id から構築。各ルートに独立した識別子
  • 識別子が異なれば表示状態も独立。ルート A で dismiss されてもルート B には表示可能
  • rulesnewTrail.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.titleconfiguration.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: ContentTypevar 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 オプションを追加。

関連セッション

コメント

GitHub Issues · utterances