ハイライト
iOS 15 と macOS Monterey で導入された Core Data
NSCoreDataCoreSpotlightDelegateは、わずか 2 行のコードで Core Data データを Spotlight に自動的にインデックス付けでき、カスタム属性セット、スタート/ストップ コントロール、全文検索をサポートします。
主要内容
ユーザーデータはアプリ内に閉じ込められる
写真タグ付けアプリを作成すると、ユーザーは数百枚の写真にタグ付けしました。ある日、ユーザーは「ナチュラル ブリッジ州立公園」の写真を見つけようとしましたが、それがどのアプリに含まれていたのか忘れてしまいました。システムの Spotlight を検索しましたが、何も見つかりませんでした。データがアプリ内に完全に閉じ込められているためです。
これはNSCoreDataCoreSpotlightDelegate解決すべき問題。 Core Data のデータ変更を自動的に監視し、コンテンツのインデックスを Spotlight に作成します。ユーザーは、システム検索でコンテンツを見つけるためにアプリを開く必要はありません。
(00:48)
このデリゲートを使用する理由
三个理由:
- Core Spotlight API との機能同等性
- 実装コードを大幅に削減(数十行から2行に)
- 追加機能を提供します: インデックス管理、更新通知、削除制御
(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 回以上呼び出され、一致するものを返しますCSSearchableItemcompletionHandler1 回だけ呼び出され、すべての結果が返された後に実行されます- クエリ構文エラーを防ぐために、ユーザー入力は検索前にエスケープする必要があります。
(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:)完了後にユーザーに確認プロンプトを表示します
関連セッション
- Build apps that share data through CloudKit and Core Data—Core DataとCloudKit間のデータ共有を学び、それをSpotlightインデックスと組み合わせてクロスデバイス検索を実現します
- Craft search experiences in SwiftUI— Spotlight インデックス データを操作するための検索インターフェイスを SwiftUI で構築する
- Add intelligence to your widgets— インテリジェントな推奨事項をウィジェットに追加し、Spotlight インデックス データとリンクします
- Meet the Screen Time API— スクリーンタイム API のコンテンツ フィルタリングとプライバシー制御
コメント
GitHub Issues · utterances