ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
AppデータのSpotlightでの表示
Core Dataでわずか2行のコードによってAppのデータをSpotlightで表示する方法を紹介します。そのデータをSpotlight検索で検出可能にする方法や、そのデータがデバイス上でユーザにどのように表示されるのかをカスタマイズする方法を確認します。最後に、Spotlightでインデックス化されたデータを使って、App内でフルテキスト検索を実装する方法も紹介します。
リソース
関連ビデオ
WWDC21
-
ダウンロード
♪ (AppデータのSpotlightでの表示) こんにちは 「AppデータのSpotlightでの表示」へ ようこそDavid Stitesです Core Dataチームのエンジニアです このセッションでは NSCoreDataCoreSpotlightDelegate を使用してSpotlightインデックスをAppに 追加する方法をお見せできることを 楽しみにしています このセッションのテーマは NSCoreDataCoreSpotlightDelegateオブジェクトと その使用目的 簡単な実装のセットアップ カスタマイズ方法について説明することです 最後に全文検索を追加してコードを検証します まず Core DataとSpotlightについてです ユーザーはApp内に多くの重要で 素晴らしいコンテンツを作成し 保存するようになるでしょう Appの利用が増え データセットのサイズが大きくなればなるほど App内でもApp外でも 例えばSpotlight検索で そのデータを素早く見つけたいと 考えるようになるだろう App内のデータがSpotlightに表示されたら 素晴らしいと思いませんか? Core Dataが役に立つのはここです NSCoreDataCore SpotlightDelegate オブジェクトは面倒な作業をすべて実行し Appによって提供されるコンテンツを迅速かつ 効率的にインデックス化する一連のAPIを提供します オンにするだけです! インデックス化された検索結果は App外部のSpotlight Search ユーザーインターフェイスにも表示されます Spotlightデレゲートは グラフの管理オブジェクトに対する 変更を自動的に処理しそれに応じて Spotlightインデックスを更新します さらにプライベートなデバイス上 のみのインデックスと対話する ための堅牢なインデックス管理機能を提供し インデックス結果を好みに合わせて調整できます 実際 永続ストアにあるコンテンツはすべて インデックス付けの対象となります Spotlightデレゲートを使用する理由は3つあります (1) Spotlightデリゲートは Core Spotlight APIと同等の機能を維持します (2) これにより必要な 実装コードが大量に削除されます (3) 優れた機能セットが追加されます この点についてこのセッションの後半で説明します 前のポイントを説明するために Core Spotlight APIを使用した非常に単純な実装 を例として挙げます検索インデックスに項目を 追加するだけでそれを...これに縮小します 2列です! シンプルで 読みやすくメンテナンスも簡単です コードを減らしたくない人なんているだろうか? すぐに設定して実行する方法を見てみましょう この簡単な例ではインデックスを 付ける対象の決定とデレゲートの 作成について説明します このセッションでは 自分で書いたTagsという シンプルな写真タグ付けAppについて説明します このサンプルAppには本日説明したAPIの多くが 組み込まれています Spotlightサポートを追加する前に 全タグと写真データがタグ内に トラップされていることを確認できます 「Natural Bridges State Park」の Spotlight検索結果がないからです それを変えましょう! NSCo reDataCoreSpotlightDelegate を使用した実装の最初のステップは Spotlightでインデックス を作成する対象を決定することです Spotlightで何をインデックス化するかはユーザ次第 TagsでエンティティPhotoのuserSpecifiedName 属性とエンティティTagのname属性に インデックスを付けることにしました インデックスを作成するための モデルを準備するために プロジェクトのCore DataモデルをXcodeで開き インデックスを作成する各属性を選択し 属性インスペクタのIndex in Spotlight チェックボックスをオンにしました Core Data Spotlight表示名を設定する 必要があるため Core Dataモデルエディタで作業を続けます Core Data Spotlightの表示名はNSTemptionです インデックス作成時に このエクスプレッションはSpotlight によってインデックス付けされた プロパティを持つ各管理オブジェクトで評価され 結果が保存されます その後Spotlight検索ユーザーインターフェイスが 表示されるとこれらの保存された結果が検索結果の 「表示名」として使用されます NSExpressionとは何ですか? エクスプレッションはキーパス この場合はTag.nameを評価する単純なものです しかしこのオブジェクトには キーパスの評価以外にもで きることがたくさんあります この例では計算を行っています さらに 数値のセットの標準偏差を計算するなど エクスプレッションを 複雑にすることもできます TagsではSpotlight Display Nameは エンティティ「Photo」 ではuserSpecifiedName エンティティ「Tag」 ではNameに設定されます モデルのインデックス作成の準備ができたので 次はSpotlightデレゲートを作成しましょう iOS 15とmacOS Montereyで始まった イニシャライザーforStoreWith: model は非推奨になりました Spotlightデレゲートを初期化する新しい方法は forStoreWith: coordinator:を使用することです 新しい指定イニシャライザを採用することで コーディネータにストアを追加する前に Spotlightデレゲートのインスタンスを ストアオプションに追加する必要がなくなりました ただし Spotlightデレゲートの作業を開始するには startSpotlightIndexingを呼び出す必要がある NSCoreDataCoreSpotlightDelegate を使用する上でのいくつかの要件を 紹介したいと思います インデックスを作成するストアの ストアタイプはSQLiteで 永続的な履歴追跡が有効に なっている必要があります そしてそれで 終わってしまいます これで完了です! 他に何もする必要はなく Spotlightでデータのインデックスが作成されます これまでTags AppにSpotlightのインデックス を追加するのがいかに簡単かを実演してきました 基本的なことを説明したので この実装を少しカスタマイズしましょう 実装をカスタマイズする最初の方法は メインとインデックス名を定義することです まず始めにNSCoreDataCoreSpotlightDelegate のサブクラスであるTagsSpotlightDelegate というクラスを定義します では domainNameとindexName を実装でオーバーライドしてみます これらのセレクタをオーバーライド することでSpotlightに インデックス付きデータの保存場所が指示され 特に複数のインデックスがある 場合は後で識別しやすくなります domainIdentifierをオーバーライドしない場合 デフォルトのドメイン識別子はストア識別子です indexNameをオーバーライドしない場合 デフォルトのインデックス名はnilです Spotlightデレゲートをカスタマイズする次の手順は 属性セットを定義することです このセッションのセットアップ部分では NSCoreDataCore SpotlightDelegate オブジェクトはSpotlight Indexチェックボックスをオン にするだけで属性セット 「Spotlightに返される」 を定義しました 次にインデックス作成に使用する属性を 指定する方法を具体的に説明します インデックスを作成する属性を指定すると インデックスの対象と検索方法を より明確にコントロールできます これを行うには CSSearchableItemAttributeSetを使用 属性セットには指定した管理対象オブジェクトが 検索結果として表示されたときに そのオブジェクトについて表示する メタデータを指定できる 多数の定義済みプロパティが含まれています 選択する属性はドメインによって完全に異なります CSSearchableItemAttributeSet に用意されている定義済みの プロパティを使うこともできますし 独自のプロパティを定義することもできます Tags Appは定義済みのプロパティ キーワードdisplayName とthumbnailDataを使用します 属性セット内のプロパティへの 同時アクセスの動作は 定義されていない為一度に1つのスレッドの 属性セットのみを変更する必要があることに 注意してください TagsSpotlightDelegateクラスに戻ります attributeSet (for object:)を オーバーライドすることによって この動作を確認してみましょう オーバーライド実装では オブジェクトがPhotoタイプの オブジェクトかどうかを判断することから始めます 次にコンテンツタイプが.imageの attributeSetを初期化する 次にPhotoオブジェクトの適切な属性を使用して 属性セットのプロパティ識別子displayName およびthumbnailDataを設定します ここでPhotoオブジェクト タグセットのタグを属性セットの keywords配列に追加します ここでモデルがリレーションシップ をインデックスする場合は attributeSet (for object:) をオーバーライドしてその リレーションシップについて 特に何がインデックスされるかを定義する 必要があることに注意してください 最後に属性セットを戻します モデルはTagオブジェクトの インデックスも作成するため コードはTagのケースを処理する必要があります そのためにはcontentType .text を持つ属性セットを作成し 表示名をタグの名前に設定して 属性セットを戻します 最後の手順として前の手順で モデルエディタで設定した Core Data Spotlight表示名を削除します さらにインデックス 作成を開始および停止するための イベントループを定義します 前述のようにSpotlightデレゲートを設定すると startSpotlightIndexingがSpotlight デレゲートの作成直後に呼び出されます NSCoreDataCoreSpotlightDelegate がインデックス作成作業を実行する タイミングを正確にコントロールできるように stopSpotlightIndexing もフレームワークに追加されました これらの2つのセレクタを組み合わせて使用すると AppがCPUやディスクの負荷の高い処理を 実行している場合など必要に応じて インデックス作成処理 を開始および停止できます ではインデックスの更新が完了したときに通知を 受け取る機能を追加しましょう Spotlightでインデックス付けされた エンティティが変更されると そのインデックスは非同期に更新されます iOS 15とmacOS Montereyでは Core Dataフレームワーク がインデックス更新通知を追加した インデックスの更新が完了した時に 通知を受け取るには Spotlightデリゲートでポストされる NSCoreDataCoreSpotlightDelegate .indexDidUpdateNotificationを サブスクライブします これらの通知はsave:on NSManagedObjectContext の呼び出しを処理した後 またはバッチ処理が完了した後にポストされます 実際にやってみましょう まずインデックス作成が有効に なっているかどうかを確認します 存在する場合は登録します indexDidUpdateNotificationに 次にハンドラで通知を検査します この通知にはリモート変更通知と同様に 2つのキーと値のペアを含む userInfoディクショナリがあります1つは Spotlightデリゲートがインデックスを更新した ストアNSString UUIDですもう1つは Spotlightデリゲートがインデックスを更新した ストアの永続履歴トークンです これらの2つのキーを使用して 目的のストアが最新の永続履歴トークンまで インデックス化されているかどうかを判断できます インデックス作成が有効に なっていない場合はオブザーバとして 通知から自分を削除できます 今年まではAppがインデックス 付けしたデータを削除するには Core Spotlight APIを実装して インデックスエントリを削除するか Core Dataのクライアント グラフ全体を削除するしかなかった 重要なのはiOS 15とmacOS Montereyで 登場したCore Dataが開発者にクライアントグラフを 削除せずにSpotlightインデックスを 管理する新しい方法を提供したことだ まずコードはインデックス付けを停止します 次にdeleteSpotlightIndexを呼び出します 最後に完了ハンドラで発生したエラーを処理します このメソッドを呼び出すとCore Dataや Core Spotlightなどの下位層の依存関係から エラーが返される可能性があるため これらを処理する準備をしておく必要があります Spotlight delegateの実装をカスタマイズする 方法を説明したので Core Spotlight APIを使用してTags Appに 全文検索を追加し設定を検証してみましょう 結果は以前にインデックス化されたものになります まずUISearchResultsUpdatingプロトコルと updateSearchResults(Controller用) 関数を採用する PhotosViewControllerの拡張を定義します Tagsユーザーインタフェースには UISarchControllerがあります その検索コントローラーの 検索バーからユーザー入力を取得します ユーザー入力が空の場合は データプロバイダからすべてのイメージを取得し 検索クエリがないので コレクションビューをリロードします 次に検索クエリがある場合を扱います まずユーザー入力文字列をエスケープして サニタイズします 次にユーザーのサニタイズされた 入力文字列を使用してクエリ文字列を定義します クエリ文字列はCSSearchableItemAttributeSet オブジェクトのプロパティに 関連付けられた値を操作します この場合コードは前のステップで設定された Keywords属性で動作します 検索クエリでは 修飾子c dおよびwが 使用されています cは大文字と小文字を区別しません dは字上符の無視を表します wは単語ベースの検索です ここで作成したフォーマット済みのクエリ文字列と CSSearchableItemAttributeSet で定義されたプロパティに対応する 属性名の配列を指定して CSSearchQueryオブジェクトを作成します この検索クエリオブジェクトは SpotlightデリゲートAPIを使用して 以前にインデックスを作成したAppコンテンツを 検索するときに適用する基準を管理します foundItemsHandlerをその後設定します このハンドラは前に定義した検索クエリに 一致する項目を使用して繰り返し呼び出されます 一度だけ呼び出される問い合わせの completionHandler 内でエラーをチェックし 場合によっては何らかのエラー処理を実行します エラーがない場合は ブロックをメインキューにディスパッチし Spotlightが検出した 項目をデータプロバイダを使用して ユーザーインタフェースにロードします 最後に最重要なこととしてクエリを 開始することを忘れないでください Tags AppにSpotlightデリゲートが コンテンツをインデックスするようになったので データはApp内から解放されました! 以前に追加したタグをSpotlightで検索すると 2つの結果が返ってきます タグ名そのものとキーワード 「Natural Bridges State Park」 でタグ付けした特定の写真です まとめるとNSCoreDataCoreSpotlightDelegate とは何か そしてユーザーがSpotlight検索で あなたのApp内とApp外のコンテンツを見つけるのに どのように役立つか また Spotlightデリゲートを 素早く簡単にセットアップして 大きなコードの負担なしに インデックス作成を開始することと このリリースで利用可能な いくつかの新しいAPIを使用して Spotlightデリゲートをカスタマイズ する方法について学びました この情報が有用であることを確認し プロジェクトで NSCoreDataCoreSpotlightDelegate を採用してユーザーがコンテンツを 検索できるようにすることを検討してください 有意義なWWDCを! ♪
-
-
2:40 - Creating a NSCoreDataCoreSpotlightDelegate
let spotlightDelegate = NSCoreDataCoreSpotlightDelegate(forStoreWith: description, coordinator: coordinator) spotlightDelegate.startSpotlightIndexing()
-
5:24 - Adding a NSCoreDataCoreSpotlightDelegate to a CoreDataStack
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("###\(#function): 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("###\(#function): 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("###\(#function): Failed to pin viewContext to the current generation:\(error)") } return container }() }
-
6:24 - Creating TagsSpotlightDelegate
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 object as? Tag { let attributeSet = CSSearchableItemAttributeSet(contentType: .text) attributeSet.displayName = tag.name return attributeSet } return nil } }
-
9:51 - Customizing PhotosViewController with Spotlight delegate functionality
class PhotosViewController: UICollectionViewController { @IBOutlet var generateDefaultPhotosItem: UIBarButtonItem! @IBOutlet var deleteSpotlightIndexItem: UIBarButtonItem! @IBOutlet var startStopIndexingItem: UIBarButtonItem! private var isTagging = false private var spotlightFoundItems = [CSSearchableItem]() private static let defaultSectionNumber = 0 private var searchQuery: CSSearchQuery? var spotlightUpdateObserver: NSObjectProtocol? private lazy var spotlightIndexer: TagsSpotlightDelegate = { let appDelegate = UIApplication.shared.delegate as? AppDelegate return appDelegate!.coreDataStack.spotlightIndexer! }() override func viewDidLoad() { super.viewDidLoad() // ... toggleSpotlightIndexing(enabled: true) } @IBAction func deleteSpotlightIndex(_ sender: Any) { toggleSpotlightIndexing(enabled: false) spotlightIndexer.deleteSpotlightIndex(completionHandler: { (error) in if let err = error { print("Encountered error while deleting Spotlight index data, \(err.localizedDescription)") } else { print("Finished deleting Spotlight index data.") } }) } @IBAction func toggleSpotlightIndexingEnabled(_ sender: Any) { if spotlightIndexer.isIndexingEnabled == true { toggleSpotlightIndexing(enabled: false) } else { toggleSpotlightIndexing(enabled: true) } } private func toggleSpotlightIndexing(enabled: Bool) { if enabled { spotlightIndexer.startSpotlightIndexing() startStopIndexingItem.image = UIImage(systemName: "pause") } else { spotlightIndexer.stopSpotlightIndexing() startStopIndexingItem.image = UIImage(systemName: "play") } let center = NotificationCenter.default if spotlightIndexer.isIndexingEnabled && spotlightUpdateObserver == nil { let queue = OperationQueue.main spotlightUpdateObserver = center.addObserver(forName: NSCoreDataCoreSpotlightDelegate.indexDidUpdateNotification, object: nil, queue: queue) { (notification) in let userInfo = notification.userInfo let storeID = userInfo?[NSStoreUUIDKey] as? String let token = userInfo?[NSPersistentHistoryTokenKey] as? NSPersistentHistoryToken if let storeID = storeID, let token = token { print("Store with identifier \(storeID) has completed ", "indexing and has processed history token up through \(String(describing: token)).") } } } else { if spotlightUpdateObserver == nil { return } center.removeObserver(spotlightUpdateObserver as Any) } } }
-
13:13 - Adding full-text search to PhotosViewController
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"]) // Set a handler for results. This will be a called 0 or more times. searchQuery?.foundItemsHandler = { items in DispatchQueue.main.async { self.spotlightFoundItems += items } } // Set a completion handler. This will be called once. searchQuery?.completionHandler = { error in guard error == nil else { print("CSSearchQuery completed with error: \(error!).") return } DispatchQueue.main.async { self.dataProvider.performFetch(searchableItems: self.spotlightFoundItems) self.reloadCollectionView() self.spotlightFoundItems.removeAll() } } // Start the query. searchQuery?.start() } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。