ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
TipKitによる機能検索のカスタマイズ
機能検索に焦点を当てたTipKitフレームワークを使用すると、アプリにヒントを簡単に表示できます。ヒントをグループ化することで、最適な順序で機能をユーザーに見つけてもらうことができます。さらに、カスタムのヒント識別子を使用してヒントを再使用可能にしたり、見た目や操作性をアプリに合わせたり、CloudKitを使用してヒントを同期したりすることも可能です。TipKitの最新機能を使用して、アプリのさまざまなサービスをユーザーに知ってもらう方法を学びましょう。
関連する章
- 0:00 - Introduction
- 1:18 - Tip groups
- 5:12 - Reusable tips with custom identifiers
- 8:25 - Custom tip styles
- 10:48 - Sync tips with CloudKit
リソース
関連ビデオ
WWDC23
-
ダウンロード
こんにちは Jakeです 今回は TipKitをカスタマイズして アプリの新機能や 知られていない機能を ユーザーに学んでもらう 新しい方法をご紹介します
TipKitは アプリ内で簡単にヒントを 表示するためのフレームワークです 新機能についてユーザーに説明したり タスクをより迅速に完了する方法を 示したりできます TipKitではヒントを簡単に作成し 表示の状態と履歴を自動的に管理して 常に適切なタイミングでのみ 表示されるようにすることができます 表示基準ルールや表示頻度を指定して いつ誰にヒントを表示するかを 制御することもできます TipKitには様々な 表示スタイルが用意されており プラットフォームを問わず アプリに最適なスタイルを選択できます さらに現在では 機能の検索をカスタマイズして ヒントが統合されシームレスに 表示できるようになりました このセッションではまず ヒントをグループ化して 理想的な順序で機能を 発見される方法を説明します 次に カスタムのヒント識別子を使って ヒントを再使用可能にする方法を紹介します さらに TipViewStyleを使用して ヒントを アプリの見た目や操作性に合わせます 最後に CloudKitを使って TipKitデータストアを同期し ヒントの表示状態をデバイス間で 共有する方法について説明します
ではまず ヒントグループです ヒントグループを使用すると 複数のヒントを指定して 1つずつ表示することができます 特定の順序で表示することも 条件に最初に合致した ヒントを表示することもできます ここでは バックカントリーハイキングで 新しいトレイルの発見やナビゲートを サポートするアプリを更新していきます 先日 このマップに コンパスコントロールを追加しました 2つの機能を備えているので ヒントを使ってユーザーに知らせましょう 最初に作成するヒントは コンパスをタップすると マップ上に現在地が 表示される機能についてです まず 新しいTip構造体を作成し タイトル メッセージ この機能を説明する画像を指定します
2つ目のヒントは 少しわかりにくい ジェスチャについてです コンパスコントロールを長押しすると マップを0度の北方向に回転して戻すこと ができるので この機能についても ヒントを追加しましょう
これらを表示するには コンパスからTipKitの popoverTipビューモディファイアへ 2回呼び出しを行います 1つはshowLocationTip用
もう1つはrotateMapTip用です
これらのポップオーバーは問題なく機能しますが ただ 1つ問題があります 現時点では ヒントが表示される順序を 制御できません 今回は コンパスの長押しに関する ヒントを表示する前に コンパスのタップ機能について ユーザーに知ってもらいたいのです では このコードに TipGroupを追加しましょう
表示を制御するため コンパスに関する両方のヒントを TipGroupに追加し 優先度としてorderedを 指定して初期化します orderedの優先度を使用すると ShowLocationTipが無効になるまで RotateMapTipが表示されなくなります ShowLocationTipが無効になるのは このビューが閉じられるか ユーザーが現在地の表示 タップジェスチャを実行したときです あとはコンパスのpopoverTip ビューモディファイアで currentTipプロパティを使用するだけで このグループの現在利用可能な ヒントが表示されるようになります
TipGroupのcurrentTipプロパティを 特定のタイプにキャストして ヒントの表示場所を カスタマイズすることもできます コンパスの両方の機能に それぞれボタンを設けている場合 currentTip as? ShowLocationTipを 使用すると 1番目のボタンでのみ 対応するヒントの ポップオーバーが表示されます RotateMapTipのポップオーバーも 2番目のボタンでのみ表示されます
これでコンパスの両方のヒントが 正しい順序で表示されるようになりました あと1つだけ追加することがあります どちらのヒントも その機能が使用された時点で 無効にする必要があります これにより その機能を 既に知っているユーザーには ヒントが表示されなくなります また 優先度としてorderedを 使用しているTipGroupの場合 ヒントが表示されるのは 先行するすべてのヒントが 無効になったときのみです TipGroupに設定できる優先度には orderedとfirstAvailableがあります orderedは このcompassTipsで使用されているように 関連する機能をユーザーに 段階的に紹介する時に役立ちます
firstAvailableでは 表示ルールを満たしている 最初のヒントが表示されます これは 関連性のない複数のヒントがあり 一度に1つのヒントを 表示したい場合に役立ちます TipGroupでは displayFrequencyも使用できます このアプリでは ヒントの displayFrequencyを週1回に設定します ヒントが表示される前に ハイカーが そのコンパス機能を自分で見つける 時間を与えるためです TipKitのdisplayFrequencyを使用すると アプリの初回起動時に 多数のヒントを表示して ユーザーを困惑させる事態を回避できます ヒントグループは ヒントを1つずつ 希望する順番で 表示することができる優れた機能です
ヒントグループに加えて 表示ルールと表示頻度を使用すると 一度に多数のヒントを表示することなく アプリの機能を段階的に紹介できます
次は カスタムの識別子を使って ヒントを再使用可能にする方法です ヒントのステータスとルールは その識別子で一意に特定されます ヒントのデフォルトの識別子を オーバーライドすると その内容に基づいて 同じヒント構造体を 再使用できるようになります 最近 このトレイルアプリには 新しいトレイルの サポートが追加されました ヒントを使用して これをユーザーに知らせたいと思います まず 新たに追加された Butler Forkトレイルヘッドのヒントを作成し その場所を示すメッセージを記述します さらに アクションボタンも追加して マップ上で新しいトレイルに 簡単に移動できるようにしましょう また このヒントを 最も役立ててもらえるハイカーにだけ 表示されるようにしたいので イベントルールを追加して このトレイルのエリアを 3回以上訪れているハイカーにのみ 表示されるようにします
ヒントを知らせるには そのインスタンスをTrailListに追加し TipViewを使用して表示するだけです アクションハンドラも追加しましょう ユーザーがボタンをタップした時 新しいトレイルがハイライトされます
今後 さらに新しいトレイルのサポートを 追加する場合はどうでしょうか? おっと 新しいトレイルをアプリに追加し続けると TrailListのコードが ほぼヒントになってしまいます また 複数のTipViewが同時に 表示される可能性もあります その場合 ユーザーは実際のトレイル リストにスムーズにアクセスできません この方法では拡張性に乏しいので コードを更新し カスタム識別子を使って ヒントを再使用できるようにしましょう
まず 新しいヒントを定義します 特定のトレイルオブジェクトを使って 作成したヒントに そのトレイルの名前とエリアに 基づくメッセージを追加します 次に 初期化に使用したトレイルに基づいて そのヒントのカスタム識別子を追加します 識別子をカスタマイズすることにより 紹介するトレイルに基づいて NewTrailTipの各インスタンスに 一意のステータスとルールが 割り当てられます これで 以前に別のトレイルで ヒントが無効になっている場合でも 新しいトレイルでは 再び表示されるようになります さらに これらのヒントを 対象エリアに関心がある ハイカーにのみ表示するため 新しく追加されたトレイルの エリアに基づいて didVisit表示ルールを更新します
あとは 最新のトレイルに基づいて 新しいヒントが 作成されるように TrailListコードを変更するだけです これで アプリに新しいトレイルを 追加するたびに ヒントを自動的に表示できます ヒントのインスタンスを 1つ作成するだけなので 複数のNewTrailTipが 同時に表示されるケースを 心配する必要がありません それぞれの識別子に基づいて ヒントごとに 永続的なレコードが作成されます 一度も表示されない場合も同様です これによりTipKitでは アプリを起動し直しても 発生するイベントに基づいて ヒントを表示することができます そのため カスタム識別子を指定するときは ユーザーIDやトレイル名など 具体的な要素を 取り入れることが重要です
デフォルトでは ヒントの識別子は 初期化時に使用したタイプ名です この識別子をオーバーライドすると それぞれの 内容に基づいてヒントを再使用できます
TipKitのカスタム識別子は 複数の異なるヒントで同じヒントモデルを 再使用するための優れた方法です
次は ヒントビューの外観の カスタマイズについてです
ヒントにはデフォルトの外観が 用意されていますが アプリのUIに合わせるため より細かいカスタマイズが 必要になることもあります そのような場合は TipViewStyleを使用して ヒントの外観と挙動をカスタマイズできます このアプリに追加するトレイルには いずれも美しい写真が用意されています NewTrailTipを使って これらの写真を紹介したいと思います では カスタムTipViewStyleを作成しましょう 各トレイルのヒーロー画像を 背景として使用し その画像上にヒントのタイトルと メッセージを表示します
タイトルとメッセージでは ヒントのインスタンス値ではなく makeBody関数の configuration引数から 取得するプロパティを使用します これにより TipViewに適用する すべてのモディファイアが カスタムスタイルのメッセージと タイトルで機能するようになります
あとはtipViewStyleモディファイアを 呼び出すだけで適用できます これで トレイルの美しい写真を背景に ヒントが表示されるようになりました NewTrailTipには 対象のトレイルをマップ上で すばやくハイライトする機能もあります しかし そのためのボタンを 写真の上に配置したくありません
カスタムスタイルを変更して ヒントビュー全体をタップ可能にしましょう
まず NewTrailTipのアクションを configuration引数から取得します あとはヒントビューがタップされた時に アクションハンドラを呼び出すだけです configuration引数の actionsプロパティを使用すると TipViewの一部として作成したハンドラが このアクションの実行時に 呼び出されるようになります
カスタムTipViewStyleを作成する時は ヒントのインスタンス値より configuration引数のプロパティを できる限り優先させることが重要です
そうすれば カスタムスタイルを 使用する場合でも TipViewに適用したクロージャと モディファイアが評価されます
カスタムTipViewStyleは tipCornerRadiusやtipBackgroundなど ヒントビューの他の モディファイアとも併用できます UIKitまたはAppKitを使用するアプリでは viewStyleプロパティを設定して TipUIViewとTipNSViewの スタイルを変更できます TipViewStyleを作成すると TipKitのルールエンジンで ヒントの表示と消去を処理しつつ 外観と挙動をカスタマイズした ヒントを簡単に表示できます
次は CloudKitの同期についてです CloudKitはヒントの 表示状態を同期することで アプリのユーザー体験を向上させます ユーザーは一度消去したヒントを 別のデバイスで 再度消去する必要がありません さて トレイルアプリにいくつかの 便利なヒントを追加したので 次はCloudKit同期を設定し ヒントのステータスやルールを 共有できるようにしましょう まず Xcodeプロジェクトの に iCloudを追加します iCloud Servicesで をオンにしたら ヒントを同期するための 新しいコンテナを作成します バックグラウンドモードも 追加する必要があります 機能を 有効にしましょう リモートでの変更をTipKitが バックグラウンドで処理できるようになり トレイルに関するアプリのヒントの ステータスと表示状態が 常に適切に維持されます
最後のステップとして Tips.configureの呼び出しに cloudKitContainerオプションを追加して 新しいコンテナの識別子を渡します
これで完了です トレイルのヒントがデバイス間で 同期されるようになりました 同じヒントを何度も 消去する必要はありません また TipKitではイベントや パラメータの値も同期されるので あるデバイスにNewTrailTipを 表示させるドネーションが 共有され 他のデバイスでも表示できるようになります
アプリでは 無効になるまでに ヒントが数回しか 表示されないことがあります ヒントを永続化すると 表示できる状態になるまで ヒントモデルが メモリに読み込まれません またCloudKit同期によって ヒントの ステータスとルールが共有されるため あるデバイスで消去したヒントは 別のデバイスで再表示されません
TipKitではイベントやパラメータも同期される ので イベントのドネーションに基づき 複数のデバイスにまたがる 表示ルールを作成できます 表示回数と表示時間も同期されます そのため MaxDisplayCountが 指定されているヒントは 全デバイスでの合計表示回数に 基づいて無効にできます
場合によっては CloudKit同期を使用しつつ 一部のヒントはプラットフォームごとに 表示したいこともあります UIDeviceを使ってプラットフォーム固有の ヒント識別子を作成すれば 複数のデバイスに 同じヒントを再表示できます
テストでは TipKitのresetDatastore関数を使用すると ローカルデータストアに加え すべてのヒントの CloudKitレコードをクリアできます
TipKitは SwiftDataの強力な 永続性を基盤として構築されています そのため アプリをいつどこで起動しても ヒントのステータス ルール パラメータ イベントの 値が保持されます CloudKit同期により デバイス間での これらの値の共有がさらに容易になります
TipKitには アプリのヒントを 最も必要としているユーザーに 最適なタイミングで 表示するための パワフルなツールが用意されています TipGroupを使用すると アプリの機能を理想的な順番で 1つずつ紹介できます TipGroupで表示ルールと 表示頻度を使用すると アプリでの機能の 紹介方法をカスタマイズできます 表示ルールの作成について詳しくは 昨年のWWDCセッション「Make features discoverable with TipKit」をご覧ください
カスタム識別子を使用すると 再使用可能なヒントモデルを簡単に作成し その内容に基づいて ヒントを再表示できます アプリのUIに合わせてヒントのレイアウトや インタラクションをカスタマイズするには TipViewStyleを使用します TipKitのデータストアを デバイス間で同期するのがCloudKitです これにより ヒントが不必要に 再表示されなくなります
本日はありがとうございました TipKitチームを代表してお礼申し上げます 皆さんのアプリの素晴らしい新機能の発見を TipKitがどのように手助けするのか とても楽しみにしています
-
-
1:43 - Create new tips
// Create new tips struct ShowLocationTip: Tip { var title: Text { Text("Show your location") } var message: Text? { Text("Tap the compass to highlight your current location on the map.") } var image: Image? { Image(systemName: "location.circle") } }
-
1:54 - Create new tips
// Create new tips struct ShowLocationTip: Tip { var title: Text { Text("Show your location") } var message: Text? { Text("Tap the compass to highlight your current location on the map.") } var image: Image? { Image(systemName: "location.circle") } } struct RotateMapTip: Tip { var title: Text { Text("Reorient the map") } var message: Text? { Text("Tap and hold on the compass to rotate the map back to 0° North.") } var image: Image? { Image(systemName: "hand.tap") } }
-
2:09 - Show popover tips
// Show popover tips struct MapCompassControl: View { let showLocationTip = ShowLocationTip() let rotateMapTip = RotateMapTip() var body: some View { CompassDial() .popoverTip(showLocationTip) .popoverTip(rotateMapTip) .onTapGesture { showCurrentLocation() } .onLongPressGesture(minimumDuration: 0.1) { reorientMapHeading() } } }
-
2:41 - Create a TipGroup
// Create a TipGroup 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() } } }
-
3:15 - Show TipGroup tips on different views
// Show TipGroup tips on different views struct MapControlsStack: View { @State var compassTips: TipGroup(.ordered) { ShowLocationTip() RotateMapTip() } var body: some View { VStack { ShowLocationButton() .popoverTip(compassTips.currentTip as? ShowLocationTip) RotateMapButton() .popoverTip(compassTips.currentTip as? RotateMapTip) } } }
-
3:50 - Invalidate tips
// Invalidate tips struct MapCompassControl: View { @State var compassTips: TipGroup(.ordered) { showLocationTip rotateMapTip } var body: some View { CompassDial() .popoverTip(compassTips.currentTip) .onTapGesture { showLocationTip.invalidate(reason: .actionPerformed) showCurrentLocation() } .onLongPressGesture(minimumDuration: 0.1) { rotateMapTip.invalidate(reason: .actionPerformed) reorientMapHeading() } } }
-
5:37 - Create a tip
// Create a tip struct ButlerForkTip: Tip { var title: Text { Text("Butler Fork is now available") } var message: Text? { Text("To see key trail info, tap Big Cottonwood Canyon on the map.") } var actions: [Action] { Action(title: "Go there now") } var rules: [Rule] { #Rule(Region.bigCottonwoodCanyon.didVisitEvent) { $0.donations.count > 3 } } }
-
6:01 - Show a TipView
// Show a TipView struct ButlerForkTip: Tip { var title: Text { Text("Butler Fork is now available") } var message: Text? { Text("To see key trail info, tap Big Cottonwood Canyon on the map.") } var actions: [Action] { Action(title: "Go there now") } var rules: [Rule] { #Rule(Region.bigCottonwoodCanyon.didVisitEvent) { $0.donations.count > 3 } } } struct TrailList: View { var trails: [Trail] var body: some View { ScrollView { let butlerForkTip = ButlerForkTip() TipView(butlerForkTip) { _ in highlightButlerForkTrail() } ListSection(title: "Trails", trails: trails) } } }
-
6:45 - Create a reusable tip
// Create a reusable tip 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 } } }
-
7:26 - Show a TipView
// Show a TipView 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 } } } struct TrailList: View { var trails: [Trail] let newTrail: Trail var body: some View { ScrollView { let newTrailTip = NewTrailTip(newTrail: newTrail) TipView(newTrailTip) { _ in highlightTrail(newTrailTip) } ListSection(title: "Trails", trails: trails) } } }
-
8:55 - Create a custom TipViewStyle
// Create a custom TipViewStyle struct NewTrailTipViewStyle: TipViewStyle { func makeBody(configuration: Configuration) -> some View { let tip = configuration.tip as! NewTrailTip TrailImage(imageName: tip.newTrail.heroImage) .frame(maxHeight: 150) .overlay { VStack { configuration.title.font(.title) configuration.message.font(.subheadline) } } } } extension NewTrailTipViewStyle { struct TrailImage: View { let imageName: String var body: some View { Image(imageName) .resizable() .aspectRatio(contentMode: .fill) } } }
-
9:20 - Apply a TipViewStyle
// Apply a TipViewStyle struct NewTrailTipViewStyle: TipViewStyle { func makeBody(configuration: Configuration) -> some View { let tip = configuration.tip as! NewTrailTip TrailImage(imageName: tip.newTrail.heroImage) .frame(maxHeight: 150) .overlay { VStack { configuration.title.font(.title) configuration.message.font(.subheadline) } } } } extension NewTrailTipViewStyle { struct TrailImage: View { let imageName: String var body: some View { Image(imageName) .resizable() .aspectRatio(contentMode: .fill) } } } struct TrailList: View { var trails: [Trail] let newTrail: Trail var body: some View { ScrollView { let newTrailTip = NewTrailTip(newTrail: newTrail) TipView(newTrailTip) { _ in highlightTrail(newTrailTip) } .tipViewStyle(NewTrailTipViewStyle()) ListSection(title: "Trails", trails: trails) } } }
-
9:45 - Add the tip's action handler
// Apply a TipViewStyle 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) } } } } } extension NewTrailTipViewStyle { struct TrailImage: View { let imageName: String var body: some View { Image(imageName) .resizable() .aspectRatio(contentMode: .fill) } } } struct TrailList: View { var trails: [Trail] let newTrail: Trail var body: some View { ScrollView { let newTrailTip = NewTrailTip(newTrail: newTrail) TipView(newTrailTip) { _ in highlightTrail(newTrailTip) } .tipViewStyle(NewTrailTipViewStyle()) ListSection(title: "Trails", trails: trails) } } }
-
11:38 - Add CloudKit sync for tips
// Add CloudKit sync for tips @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)") } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。