WWDC Quick Look 💓 By SwiftGGTeam
Showcase app data in Spotlight

Showcase app data in Spotlight

元の動画を見る

ハイライト

iOS 15 と macOS Monterey で導入された Core DataNSCoreDataCoreSpotlightDelegateは、わずか 2 行のコードで Core Data データを Spotlight に自動的にインデックス付けでき、カスタム属性セット、スタート/ストップ コントロール、全文検索をサポートします。

主要内容

ユーザーデータはアプリ内に閉じ込められる

写真タグ付けアプリを作成すると、ユーザーは数百枚の写真にタグ付けしました。ある日、ユーザーは「ナチュラル ブリッジ州立公園」の写真を見つけようとしましたが、それがどのアプリに含まれていたのか忘れてしまいました。システムの Spotlight を検索しましたが、何も見つかりませんでした。データがアプリ内に完全に閉じ込められているためです。

これはNSCoreDataCoreSpotlightDelegate解決すべき問題。 Core Data のデータ変更を自動的に監視し、コンテンツのインデックスを Spotlight に作成します。ユーザーは、システム検索でコンテンツを見つけるためにアプリを開く必要はありません。

00:48

このデリゲートを使用する理由

三个理由:

  1. Core Spotlight API との機能同等性
  2. 実装コードを大幅に削減(数十行から2行に)
  3. 追加機能を提供します: インデックス管理、更新通知、削除制御

02:05

2 つのステップでインデックスを開く

ステップ 1: Core Data モデル エディターで、インデックスを作成する属性の [Spotlight のインデックス] をチェックし、Spotlight 表示名 (NSExpression で定義) を設定します。

ステップ 2: デリゲートを作成し、インデックス作成を開始します。

let spotlightDelegate = NSCoreDataCoreSpotlightDelegate(
    forStoreWith: description,
    coordinator: coordinator
)
spotlightDelegate.startSpotlightIndexing()

キーポイント:

  • ストレージ タイプは SQLite である必要があります
  • 永続的な履歴追跡を有効にする必要があります
  • iOS 15以降で利用可能forStoreWith:coordinator:イニシャライザ、古いforStoreWith:model:廃止されました

02:40

詳細

自定义 Spotlight 索引

基本的な実装でも十分ですが、カスタマイズするとより柔軟になります。サブクラスを作成して、ドメイン、インデックス名、および属性セットを定義します。

class TagsSpotlightDelegate: NSCoreDataCoreSpotlightDelegate {
    override func domainIdentifier() -> String {
        return "com.example.apple-samplecode.tags"
    }

    override func indexName() -> String? {
        return "tags-index"
    }

    override func attributeSet(for object: NSManagedObject) -> CSSearchableItemAttributeSet? {
        if let photo = object as? Photo {
            let attributeSet = CSSearchableItemAttributeSet(contentType: .image)
            attributeSet.identifier = photo.uniqueName
            attributeSet.displayName = photo.userSpecifiedName
            attributeSet.thumbnailData = photo.thumbnail?.data

            for case let tag as Tag in photo.tags ?? [] {
                if let name = tag.name {
                    if attributeSet.keywords != nil {
                        attributeSet.keywords?.append(name)
                    } else {
                        attributeSet.keywords = [name]
                    }
                }
            }
            return attributeSet
        } else if let tag = object as? Tag {
            let attributeSet = CSSearchableItemAttributeSet(contentType: .text)
            attributeSet.displayName = tag.name
            return attributeSet
        }
        return nil
    }
}

キーポイント:

  • domainIdentifier異なるインデックスを区別する。複数のインデックスがある場合に特に重要
  • indexNameその後の管理を容易にするためにインデックスに名前を付けます
  • attributeSet(for:)Spotlight に表示されるメタデータを管理オブジェクトごとに定義する
  • モデルがリレーションシップ属性にインデックスを作成する場合、このメソッドをオーバーライドして、リレーションシップのどのフィールドがインデックスに参加するかを定義する必要があります。
  • CSSearchableItemAttributeSet同時アクセスには未定義の動作があり、単一スレッドの変更が行われます

06:24

在 Core Data Stack 中集成

import Foundation
import CoreData

class CoreDataStack {
    private(set) var spotlightIndexer: TagsSpotlightDelegate?

    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "Tags")

        guard let description = container.persistentStoreDescriptions.first else {
            fatalError("Failed to retrieve a persistent store description.")
        }

        description.type = NSSQLiteStoreType
        description.setOption(true as NSNumber,
            forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        description.setOption(true as NSNumber,
            forKey: NSPersistentHistoryTrackingKey)

        container.loadPersistentStores(completionHandler: { (_, error) in
            guard let error = error as NSError? else { return }
            fatalError("Failed to load persistent stores: \(error)")
        })

        spotlightIndexer = TagsSpotlightDelegate(
            forStoreWith: description,
            coordinator: container.persistentStoreCoordinator
        )

        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        container.viewContext.automaticallyMergesChangesFromParent = true

        do {
            try container.viewContext.setQueryGenerationFrom(.current)
        } catch {
            fatalError("Failed to pin viewContext: \(error)")
        }

        return container
    }()
}

キーポイント:

  • NSPersistentHistoryTrackingKey有効にする必要があります。デリゲートは変更を追跡するために履歴に依存します
  • NSPersistentStoreRemoteChangeNotificationPostOptionKeyリモート変更通知を有効にする
  • 代表者が入っていますloadPersistentStores後で作成されますが、データを変更する前にインデックスを開始する必要があります

05:24

開始/停止制御と更新通知

class PhotosViewController: UICollectionViewController {
    private var spotlightUpdateObserver: NSObjectProtocol?
    var spotlightIndexer: TagsSpotlightDelegate!

    override func viewDidLoad() {
        super.viewDidLoad()
        toggleSpotlightIndexing(enabled: true)
    }

    private func toggleSpotlightIndexing(enabled: Bool) {
        if enabled {
            spotlightIndexer.startSpotlightIndexing()
        } else {
            spotlightIndexer.stopSpotlightIndexing()
        }

        let center = NotificationCenter.default
        if spotlightIndexer.isIndexingEnabled && spotlightUpdateObserver == nil {
            spotlightUpdateObserver = center.addObserver(
                forName: NSCoreDataCoreSpotlightDelegate.indexDidUpdateNotification,
                object: nil,
                queue: .main
            ) { notification in
                let userInfo = notification.userInfo
                let storeID = userInfo?[NSStoreUUIDKey] as? String
                let token = userInfo?[NSPersistentHistoryTokenKey] as? NSPersistentHistoryToken
                print("Store \(storeID ?? "") indexed up to token \(String(describing: token))")
            }
        } else if !enabled {
            if let observer = spotlightUpdateObserver {
                center.removeObserver(observer)
                spotlightUpdateObserver = nil
            }
        }
    }

    @IBAction func deleteSpotlightIndex(_ sender: Any) {
        toggleSpotlightIndexing(enabled: false)
        spotlightIndexer.deleteSpotlightIndex { error in
            if let err = error {
                print("Error deleting index: \(err.localizedDescription)")
            } else {
                print("Spotlight index deleted.")
            }
        }
    }
}

キーポイント:

  • startSpotlightIndexing()そしてstopSpotlightIndexing()インデックス作成のタイミングを制御し、アプリが重い CPU またはディスク操作を実行するときに一時停止するのに適しています
  • indexDidUpdateNotification存在するsave:または、バッチ操作の完了後に非同期的に公開します。
  • 通知の userInfo には次の内容が含まれますNSStoreUUIDKey(UUID を保存) およびNSPersistentHistoryTokenKey(歴史的トークン)
  • deleteSpotlightIndexiOS 15 の新機能、Core Data データを削除せずにインデックスをクリアできるようになりました

09:51

Spotlight インデックスを使用したアプリ内全文検索

インデックス作成が完了すると、追加のストレージを必要とせずに、同じデータをアプリ内の全文検索に使用できます。

extension PhotosViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        guard let userInput = searchController.searchBar.text, !userInput.isEmpty else {
            dataProvider.performFetch(predicate: nil)
            reloadCollectionView()
            return
        }

        let escapedString = userInput
            .replacingOccurrences(of: "\\", with: "\\\\")
            .replacingOccurrences(of: "\"", with: "\\\"")
        let queryString = "(keywords == \"" + escapedString + "*\"cwdt)"

        searchQuery = CSSearchQuery(
            queryString: queryString,
            attributes: ["displayName", "keywords"]
        )

        searchQuery?.foundItemsHandler = { items in
            DispatchQueue.main.async {
                self.spotlightFoundItems += items
            }
        }

        searchQuery?.completionHandler = { error in
            guard error == nil else {
                print("CSSearchQuery error: \(error!)")
                return
            }
            DispatchQueue.main.async {
                self.dataProvider.performFetch(searchableItems: self.spotlightFoundItems)
                self.reloadCollectionView()
                self.spotlightFoundItems.removeAll()
            }
        }

        searchQuery?.start()
    }
}

キーポイント:

  • CSSearchQuery独自の検索ロジックを構築せずに、Spotlight インデックスを直接クエリします。
  • クエリ文字列の構文:keywords == "搜索词*"cwdt
  • 修飾子:c大文字と小文字は区別されません。dアクセントを区別しないでください。w単語に基づいて照合する
  • foundItemsHandler0 回以上呼び出され、一致するものを返しますCSSearchableItem
  • completionHandler1 回だけ呼び出され、すべての結果が返された後に実行されます
  • クエリ構文エラーを防ぐために、ユーザー入力は検索前にエスケープする必要があります。

13:13

重要ポイント

1.メモ作成アプリに Spotlight インデックスを追加

  • 機能: ユーザーがシステム検索でメモのコンテンツを直接検索できるようにします。
  • 実行する価値がある理由: Note アプリの核となる価値は高速な検索であり、Spotlight インデックスは検索範囲をシステム レベルまで拡張します。
  • 開始方法: Core Data モデルのノート タイトルとコンテンツ プロパティの [Spotlight でインデックス] をチェックし、デリゲートを作成してインデックス作成を開始します。

2.独自に構築した検索の代わりに Spotlight インデックスを使用します

  • 対処方法: 別の検索データベースを維持する必要はなくなり、直接使用します。CSSearchQuerySpotlight インデックスのクエリ
  • 実行する価値がある理由: コードの量を削減し、検索パフォーマンスがシステムによって最適化され、ピンイン、あいまい一致などを自動的にサポートします。
  • 開始方法: 既存の検索ロジックを次のものに置き換えます。CSSearchQuery、使用foundItemsHandlerそしてcompletionHandler処理結果

3.写真管理アプリにタグ検索を追加

  • 機能: ユーザーがタグを使用して Spotlight 内の写真を検索できるようにします
  • 価値がある理由: タグは、大量の写真がある場合に検索する最も効果的な方法です。 Spotlight インデックスにより、アプリ間でタグ検索が可能になります
  • 開始方法:attributeSet(for:)タグ名を追加しますkeywords配列。ユーザーは任意のタグ名を検索して、対応する写真を見つけることができます

4.オンデマンドのインデックスの開始と停止を実装します

  • 対処法: アプリが大規模なデータのインポートまたは同期を実行している間は、Spotlight インデックス作成を一時停止します。
  • 実行する価値がある理由: インデックス作成操作は CPU とディスク I/O を消費します。パフォーマンスが重視される期間に一時停止すると、遅延を回避できます。
  • 開始方法: データのインポートが開始される前に呼び出されますstopSpotlightIndexing()、インポートの完了後に呼び出されますstartSpotlightIndexing()

5.ユーザーにプライバシー管理を提供するためのインデックスの削除

  • 何をすべきか: 設定ページに「検索インデックスのクリア」オプションを追加します。
  • 実行する価値がある理由: iOS 15 の新機能deleteSpotlightIndexユーザーはデータを削除せずに検索インデックスをクリアできるため、プライバシーのニーズに対応できます。
  • 開始方法: 設定にスイッチを追加して呼び出しますdeleteSpotlightIndex(completionHandler:)完了後にユーザーに確認プロンプトを表示します

関連セッション

コメント

GitHub Issues · utterances