
こんにちは。トモニテでiOSアプリを開発している國吉です。
トモニテではサテライトを含め、複数のアプリをリリースしています。それぞれアプリのリリースが終えてからグロースするために改善/運用を行っていますが、N1インタビュー等でユーザーの声を聞いてみると「そんな機能があったんですね!知らなかったです」という意見がちらほら見受けられます。我々機能を提供している側としては、できるだけ機能認知してもらえるようにデザインを工夫して提供していても一定数は機能にたどり着く前に離脱してしまう可能性があります。
そこで今回はiOS17で追加されたTipKitを用いて、機能誘導できないか検証がてら触れていきたいと思います!
TipKitとは
まずTipKitの概要をお話しします。 TipKitはアプリ上でヒントを表示しユーザーに機能/操作方法を知ってもらうための手段として提供されています。
これにより、アップデートで追加した機能を知ってもらったり、オンボーディングでアプリのコアとなる機能を知ってもらうことができます。
ただし、ユーザーアクションを促す上で、”ヒントをどのタイミングで表示するか””どのようなメッセージでアプローチするか”を考えるのが難しく、設計がすごく大事になると思います。
実装
早速実装に取り掛かります。今回はUIKitで表示するとこまでを紹介しますが、SwiftUIでも実装はすごく簡単です。
まずはAppDelegateでTipKitを使う設定だけしておきます。
import TipKit
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Task {
try? await Tips.configure()
}
/*
.....
*/
}
次にヒントを表示したいViewControllerで処理を記述していきます。
下記ではヒントで表示するTipの中身を定義しています。
import TipKit
struct StartButtonFeatureTip: Tip {
var title: Text {
Text("陣痛が始まったと思ったらタップ!!")
}
var message: Text? {
Text("陣痛がおさまってきたら「おさまったかも」ボタンをタップして計測を終了しましょう")
}
var image: Image? {
Image(systemName: "hand.tap.fill")
}
}
動作検証では画面表示タイミングでヒントを表示したかったので、viewDidAppearで表示登録をしています。
画面から離れる際に、インスタンスを解放できるようにnilを代入しています。
// クラスプロパティ
private var startButtonFeatureTip = StartButtonFeatureTip()
private var tipObservationTask: Task<Void, Never>?
private weak var tipPopoverController: TipUIPopoverViewController?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
/*
...
*/
tipObservationTask = tipObservationTask ?? Task { @MainActor in
for await shouldDisplay in startButtonFeatureTip.shouldDisplayUpdates {
if shouldDisplay {
let popoverController = TipUIPopoverViewController(startButtonFeatureTip, sourceItem: contractStartButton)
present(popoverController, animated: animated)
tipPopoverController = popoverController
} else {
if presentedViewController is TipUIPopoverViewController {
dismiss(animated: animated)
tipPopoverController = nil
}
}
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
tipObservationTask?.cancel()
tipObservationTask = nil
}
ここまでの記述でシミュレーターを起動するとヒントが表示されるようになります。

すごく簡単ですね。次はテキストなどカスタマイズできるとこは調整しアプリに馴染むようにしていきたいと思います!
テキストカラーやフォントなどはTipの中身を定義しているとこで記述し、アイコンの色についてはTipUIPopoverViewControllerの中にあるViewのtintColorを変更することで指定できます。
struct StartButtonFeatureTip: Tip {
var title: Text {
Text("陣痛が始まったと思ったらタップ!!")
.foregroundStyle(Color(uiColor: .defaultTint))
}
var message: Text? {
Text("陣痛がおさまってきたら「おさまったかも」ボタンをタップして計測を終了しましょう")
.foregroundStyle(Color(uiColor: .defaultTint))
}
var image: Image? {
Image(systemName: "hand.tap.fill")
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
/*
...
*/
tipObservationTask = tipObservationTask ?? Task { @MainActor in
for await shouldDisplay in startButtonFeatureTip.shouldDisplayUpdates {
if shouldDisplay {
let popoverController = TipUIPopoverViewController(startButtonFeatureTip, sourceItem: contractStartButton)
popoverController.view.tintColor = .defaultTint
present(popoverController, animated: animated)
tipPopoverController = popoverController
} else {
if presentedViewController is TipUIPopoverViewController {
dismiss(animated: animated)
tipPopoverController = nil
}
}
}
}
}

ヒントのbackgroundColorを指定することもできるのですが、閉じるアイコンの色の指定は現状不明なので、注意が必要そうでした(iOS17が正式リリースされたらUIKitからでも閉じるアイコンの色も指定できるようになることを願います。。)

ヒントの中にアクションリンクを付与することも可能です。
Tipの中身の定義にActionを追加します。TipUIPopoverViewControllerのイニシャライザにactionHandlerがあるので、ハンドラー内でAction処理を記述していきます。
struct StartButtonFeatureTip: Tip {
var title: Text {
Text("陣痛が始まったと思ったらタップ!!")
.foregroundStyle(Color(uiColor: .defaultTint))
}
var message: Text? {
Text("陣痛がおさまってきたら「おさまったかも」ボタンをタップして計測を終了しましょう")
.foregroundStyle(Color(uiColor: .defaultTint))
}
var image: Image? {
Image(systemName: "hand.tap.fill")
}
var actions: [Action] {
[Action(id: "start_button", title: "詳細はこちら")]
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Tracker.track(event: .counterScreen)
AdjustTracker.track(event: .counterScreen)
let statusBarHeight = self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
headerViewHeightConstraint?.constant = 44 + statusBarHeight
tipObservationTask = tipObservationTask ?? Task { @MainActor in
for await shouldDisplay in startButtonFeatureTip.shouldDisplayUpdates {
if shouldDisplay {
let popoverController = TipUIPopoverViewController(startButtonFeatureTip, sourceItem: contractStartButton, actionHandler: { action in
guard action.id == "start_button" else { return }
/*
「詳細はこちら」をタップ後の動作
*/
})
popoverController.view.tintColor = .defaultTint
present(popoverController, animated: animated)
tipPopoverController = popoverController
} else {
if presentedViewController is TipUIPopoverViewController {
dismiss(animated: animated)
tipPopoverController = nil
}
}
}
}
}

ルールをつける
ヒントを表示したい時に「ユーザーがXXXXをしたら」「ユーザーがログインしたら」等々条件を満たした時に表示したい場合があると思います。そこで使用するのがルールです。
ルールは2種類あります。
- パラメータベース
- 感覚的には”ログインしているか”等Boolで管理できるものだと思います。
- イベントベース
- 「XXXをしたら」等のイベントトリガーを指定できます。
今回はお試しでイベントベースのルールを使用し、「きたかも」Buttonを2回タップしたら「おさまったかも」Buttonにヒントを表示する処理を書いていきます。
まずは、Tipの中身の定義にEventとRuleと記述します。
viewDidAppear内は特に変わりありません。
「きたかも」Buttonをタップされたタイミングで、startButtonTappedCountをインクリメントする必要があります。
struct StopButtonFeatureTip: Tip {
static let startButtonTappedCount = Event(id: "start_button_tapped_count")
var title: Text {
Text("陣痛がおさまったらタップ!!")
.foregroundStyle(Color(uiColor: .defaultTint))
}
var message: Text? {
nil
}
var image: Image? {
Image(systemName: "hand.tap.fill")
}
var rules: [Rule] {
#Rule(Self.startButtonTappedCount) {
$0.donations.count >= 2
}
}
}
private lazy var contractStartButton: ContractStartButton = {
let button = ContractStartButton()
button.configure(tapped: { [weak self] in
guard let self = self else { return }
Task {
try? await StopButtonFeatureTip.startButtonTappedCount.donate()
}
/*
...
*/
})
return button
}()

その他
他にも1日1個のヒントしか表示しない。同じヒントが出続けないように出現回数を制限する。等のオプションもあります。ぜひ調べてみてください。
また、見た目の確認したいなど常にヒントを出したい時もあると思いますが、その時はこのように記述することで全てのヒントを表示することができます。
Tips.showAllTipsForTesting()
最後に
今回はiOS17で追加されたTipKitを触ってみました。感想としてはオンボーディングに組み込むことで初日の機能利用を促すことができ、細かいとこまで機能認知に繋がりそうだと感じました。
また大きめな機能を追加しアップデートを実施した際にも同様に価値を感じそうです。
ただ、ベースのレイアウトはOSが提供したものに則る必要があるため、すごくリッチな見た目で表示したい要望がある場合には使えなさそうです。
トモニテではサテライトアプリで検証し、ユーザーの反応が良ければ随時、他のアプリには展開していこうかなと考えています。
参考
https://developer.apple.com/videos/play/wwdc2023/10229/
https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit