壁紙アプリの利用者から、こんな声をもらったことがあります。「お気に入りに入れたあの一枚を、ホーム画面の検索からすぐ出せたら嬉しい」。アプリを開いてタブを切り替えて探すのではなく、iPhone 全体の検索(Spotlight)から直接そのコンテンツに飛べたら、確かに気持ちが良いはずです。
これを実現するのが Core Spotlight です。アプリ内のコンテンツを iPhone の検索索引に載せ、検索結果のタップから該当画面へ復帰させられます。React Native のブリッジ越しでは細部に手が届きにくい領域ですが、Rork Max はネイティブ Swift を生成するため、CoreSpotlight を import すれば索引の登録から復帰まで素直に組めます。ここでは「探されるアプリ」にするための実装を、登録・更新・削除と本番の落とし穴を軸に整理します。
なぜ Spotlight に載せる価値があるのか
個人開発のアプリは、一度インストールされても、日々の暮らしの中で存在を忘れられがちです。私自身、AdMob 中心の無料アプリを複数運用してきて、再訪のきっかけをどう作るかが収益を左右すると痛感しています。Spotlight への索引は、ユーザーが何かを検索した瞬間に、自分のアプリのコンテンツがそっと候補に並ぶ導線です。広告でも通知でもなく、ユーザーの能動的な検索に静かに応える形なので、体験を壊しません。
ただし索引は「載せて終わり」ではありません。コンテンツが変われば索引も追従させる必要があり、ここを怠ると検索結果が古いまま残ります。
Step 1: コンテンツを CSSearchableItem として索引する
最初に、アプリ内のコンテンツを CSSearchableItem に変換して索引へ登録します。uniqueIdentifier は後で該当コンテンツを復元するための鍵になるため、アプリ内の安定した ID を使います。
import CoreSpotlight
import MobileCoreServices
func index ( wallpaper id: String , title : String , thumbnail : Data) {
let attr = CSSearchableItemAttributeSet ( contentType : . image )
attr.title = title
attr.contentDescription = "保存済みの壁紙"
attr.thumbnailData = thumbnail // 検索結果に出るサムネイル
let item = CSSearchableItem (
uniqueIdentifier : id, // 復元の鍵。安定した ID を使う
domainIdentifier : "wallpaper" , // まとめて削除する単位
attributeSet : attr
)
CSSearchableIndex. default (). indexSearchableItems ([item]) { error in
if let error = error { print ( "index failed: \( error ) " ) }
}
}
domainIdentifier を用途ごとに分けておくと、後でカテゴリ単位の一括削除が楽になります。私はこれを最初に決めずに作って、削除時にまとめて消せず苦労しました。最初の設計で区切っておくことを推奨します。
Step 2: 検索結果のタップから該当画面へ復帰する
索引したコンテンツが検索結果に出てタップされると、アプリは NSUserActivity 経由で起動します。ここで uniqueIdentifier を取り出し、対応する画面へ復帰させます。
func application ( _ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler : @escaping ([UIUserActivityRestoring] ? ) -> Void ) -> Bool {
guard userActivity.activityType == CSSearchableItemActionType,
let id = userActivity.userInfo ? [CSSearchableItemActivityIdentifier] as? String
else { return false }
Router.shared. openWallpaper ( id : id) // 該当画面へ直接遷移する
return true
}
ここで id を画面遷移に正しく渡せないと、検索結果から開いても毎回トップ画面に戻る、という残念な挙動になります。検索からの復帰は、通常のアプリ起動とは別経路だと意識して経路を分けるのが確実です。
Step 3: 追加・更新・削除に索引を追従させる
索引の鮮度を保つには、コンテンツのライフサイクルに合わせて索引を更新します。同じ uniqueIdentifier で再登録すれば内容は上書きされ、不要になったものは ID 指定で削除します。
// コンテンツが消えたら索引からも消す
func removeFromIndex ( ids : [ String ]) {
CSSearchableIndex. default ()
. deleteSearchableItems ( withIdentifiers : ids) { error in
if let error = error { print ( "delete failed: \( error ) " ) }
}
}
私が本番で踏んだ落とし穴は、ユーザーがお気に入りを解除しても索引を消していなかったことでした。結果、検索には出るのに開くと「もう無い」という不整合が起きます。コンテンツの削除処理と索引の削除を必ず同じ場所で行うように設計し直して解消しました。
Step 4: 索引のコストと再索引の戦略
索引は無制限・無料ではありません。大量のアイテムを一度に登録すると、端末の索引処理に負荷がかかり、低価格帯の端末では体感の引っかかりにつながります。私の方針は、アプリ起動のたびに全件を再索引するのではなく、変更があったものだけを差分で更新することです。初回だけ全件を登録し、以降はユーザー操作に応じて部分的に追従させると、コストを大きく抑えられます。
また、thumbnailData を大きな画像のまま渡すと索引が重くなります。検索結果に出るのは小さなサムネイルなので、あらかじめ縮小したものを渡すのが実務的です。
検索に出るまでの時間と動作確認の進め方
索引を登録しても、検索結果に反映されるまでには端末側の処理待ちがあります。登録直後にすぐ検索しても出てこないことがあり、ここで「索引に失敗した」と早合点しがちです。私は数十秒ほど待ってから確認するようにしています。それでも出ない場合に初めて、登録時のエラーや uniqueIdentifier の重複を疑います。
動作確認は実機で行うのが確実です。シミュレータでは Spotlight の挙動が実機と異なる場面があり、シミュレータだけで判断すると本番でのつまずきを見落とします。私の場合、シミュレータでは出るのに実機で出ないという食い違いに一度悩まされ、それ以降は実機での確認を必ず工程に組み込むようにしました。確認の際は、登録した title の一部を検索窓に入れ、サムネイル付きで候補に並ぶこと、タップで該当画面へ戻ることの二点を毎回見ています。
Step 5: 何を検索に出すかを絞る
技術的にはアプリ内のあらゆるものを索引できますが、出すべきは「ユーザーが名前で思い出して探すもの」に限ります。私が個人開発で採った判断は、ユーザー自身が保存・命名したお気に入りだけを索引し、システムが自動生成した一覧やテンポラリな画面は索引しないことでした。検索結果がノイズだらけになると、せっかくの導線がかえって信頼を損ないます。
「探される」状態をどう設計するか
Core Spotlight の価値は、機能の華やかさではなく、ユーザーが思い出した瞬間にアプリがそっと応えられることにあります。私自身、再訪のきっかけ作りに長く悩んできた立場として、検索という能動的な行動に静かに寄り添えるこの仕組みは、個人開発のアプリと相性が良いと感じています。Rork Max でネイティブの索引 API に手が届くようになった今、コンテンツを「探される」形に整える視点を持てるかどうかが、地味ながら再訪率に効いてくるはずです。実装の参考になれば幸いです。