iOSのSpotlight検索で自分のアプリ内コンテンツを検索対象にする方法

#Spotlight検索とは・・・？
ホーム画面で下にスワイプしたりすると出るあれです。

![スクリーンショット 2019-11-30 14.14.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152335/a461892e-d300-b099-a3b9-f2ae635b3bb3.png)

入力欄にキーワードを入力すると、各アプリのコンテンツなどから、一致するものを探してもらえるので、横断的にアプリのコンテンツを検索できるので便利です。

今回は、このSpotlight検索に、自分のアプリのコンテンツを表示させる方法についての記事です。

TodayExtensionやリッチプッシュなどのように、追加で証明書などもいらず、簡単に実装できてアプリへの導線が増やせるので、やってみてはいかがでしょうか🤔

#実装方法

##1.準備
プロジェクトにMobileCoreServiceとCoreSpotlightのFrameworkを入れます。
![スクリーンショット 2019-11-30 13.11.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152335/2a395d37-0c9d-1bd4-7d2b-4916e27a7eb6.png)


##2.保存処理を実装する

今回は、例として映画の情報である以下のMovie構造体をSpotlight検索に保存してみましょう。

```swift:Movie.swift
struct Movie {

    /// 映画固有の識別番号
    let id: Int = 0

    /// 名前
    let title: String = "スパイダーマン"

    /// あらすじ
    let summary: String = "平凡な少年、ピーター・パーカーは放射能汚染された蜘蛛に噛まれたことで、超人的な能力を得てしまう・・・"

    /// 画像
    let thumbnail: UIImage? = UIImage(named: "spider")

    /// 役者名の配列
    let actorNames: [String] = ["トビー・マグワイア", "キルスティン・ダンスト", "ジェームズ・フランコ"]
}
```

```swift:SpotlightManager.swift
import Foundation
import MobileCoreService
import CoreSpotlight

final class SpotlightManager {
    
    func save(_ movie: Movie) {
        let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String)

        // ①タイトル
        attributeSet.title = movie.title

        // ②説明文
        attributeSet.contentDescription = movie.summary
        
        // ③画像
        attributeSet.thumbnailData = movie.thumbnail.pngData()
        
        // キーワード（表示されないが、タイトルや説明文に入ってない文言をここに入れておけば、検索した時に引っかかるようになる）
        attributeSet.keywords = movie.actorNames

        /* 
         uniqueIdentifierはAppDelegateで取り出すことができるので、
         Spotlight検索経由でアプリを開いた時のためのURLスキームを入れておく
        */
        let item = CSSearchableItem(
            uniqueIdentifier: "my-app://open/movie?id=\(movie.id)",          
            domainIdentifier: "my-app",
            attributeSet: attributeSet
        )
        CSSearchableIndex.default().indexSearchableItems([item], completionHandler: nil)
    }
}
```

実際の表示された際は以下の様になります。
![スクリーンショット_2019-11-30_14_51_52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152335/d3045098-35fa-53ac-6798-977ce0464400.png)


##3.起動時の処理を実装する

```swift:AppDelegate.swift
import CoreSpotlight

extension AppDelegate {

    func application(_ application: UIApplication,
                     continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> 
Bool {
        // Spotlightで開かれたかどうかをチェックする
        switch userActivity.activityType {
        case CSSearchableItemActionType:
            return self.openApplicationFromSpotlight(userActivity)
        default:
            return false
        }
    }

    private func openApplicationFromSpotlight(_ userActivity: NSUserActivity) -> Bool {
        // userActivityからURLスキームを取得する
        guard let urlScheme = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String else {
            return false
        }
        
        // URLスキームを開いた時の処理を実装する
        return true
    }
}
```

##4.保存処理を作る
```swift:ViewController.swift

final class ViewController: UIViewController {
    
    let spotlightManager = SpotlightManager()
   
    private func show(_ movie: Movie) {
        self.spotlightManager.save(movie)
    }
}
```

##5.確認する
`title`、`description`、`keyword`に設定した文字列を検索欄に入力すると表示されます。
![スクリーンショット 2019-11-30 14.19.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152335/6d84dc45-5161-9828-d2ca-378bf593585e.png)

部分検索でもヒットします。
![スクリーンショット 2019-11-30 14.20.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152335/c12a473f-c603-ddc7-ebc3-7c05a6f800eb.png)

以上で、ざっくりとした実装は以上です。

#おまけ

今回紹介した実装では、Spotlightに保存する画像をローカルのものを使用しましたが、URLで画像を取得して表示することも可能です。
例として画像キャッシュライブラリの`Nuke`を使用した方法を記載しておきます。

```swift:SpotlightManager.swift
import Foundation
import MobileCoreService
import CoreSpotlight
import Nuke

func save(_ movie: Movie) {
    ImagePipeline.shared.loadImage(
        with: movie.thumbnailUrl,
        progress: nil) { response, _ in
            guard let thumbnail = response?.image else {
                return
            }
                
            let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String)

            attributeSet.title = movie.title

            attributeSet.contentDescription = movie.summary
            
            attributeSet.thumbnailData = thumbnail.pngData()
            
            attributeSet.keywords = movie.actorNames

            let item = CSSearchableItem(
                uniqueIdentifier: "my-app://open/movie?id=\(movie.id)",          
                domainIdentifier: "my-app",
                attributeSet: attributeSet
            )
            CSSearchableIndex.default().indexSearchableItems([item], completionHandler: nil)    
    }
}
```