The original article was posted on my blog www.fatbobman.com
This article will explain how to use NSCoreDataSpotlightDelegate (WWDC 2021 version) to realize the application of the Core Data added to the Spotlight Data index, easy search and improve the visibility of the App.
basis
Spotlight
Since launching on iOS in 2009, Spotlight has grown from apple’s official app search to an all-encompassing feature portal over the past decade, and users are increasingly using and relying on Spotligh.
Showing your application’s data in Spotlight can dramatically improve your application’s visibility.
Core Spotlight
Starting with iOS 9, Apple introduced the Core Spotlight framework, which allows developers to add content from their apps to the Spotlight index for easy access.
To create a Spotlight index for items in your application, follow these steps:
- Create a CSSearchableItemAttributeSet (attributes) object, for you to index the project Settings for metadata (attributes).
- Create a CSSearchableItem object to represent this item. Each CSSearchableItem object has a unique identifier for later reference (update, delete, rebuild)
- If necessary, you can specify a domain identifier for your projects so that multiple projects can be organized together for unified management
- Will create the above attributes (CSSearchableItemAttributeSet) associated with the search term (CSSearchableItem)
- Add searchable items to the Spotlight index of your system
The Spotlight index also needs to be updated when items in the app are changed or deleted, so that users always get valid search results.
NSUserActivity
The NSUserActivity object provides a lightweight way to describe the state of your application for later use. This object is created to capture information about what the user is doing, such as viewing application content, editing documents, viewing web pages, or watching videos.
When the user searches for your app’s content data (searchable items) from Spotlight and clicks, the system launches the app, And passed to it an item with a searchable corresponding NSUserActivity object (activityType CSSearchableItemActionType), an application can pass the information in the object, will himself restore to an appropriate condition.
For example, when a user queries an email by keyword in Spotlight, the app will directly locate the email and display its details when they click on the results.
process
With the introduction of Core Spotlight and NSUserActivity above, let’s use code snippets to briefly comb through the flow:
Create searchable items
import CoreSpotlight
let attributeSet = CSSearchableItemAttributeSet(contentType: .text)
attributeSet.displayName = Star Wars
attributeSet.contentDescription = "Once upon a time, in a galaxy far, far away, jedi knights on a righteous mission fought against the evil dark forces of the empire."
let searchableItem = CSSearchableItem(uniqueIdentifier: "starWar", domainIdentifier: "com.fatbobman.Movies.Sci-fi", attributeSet: attributeSet)
Copy the code
Add to Spotlight index
CSSearchableIndex.default().indexSearchableItems([searchableItem]){ error in
if let error = error {
print(error.localizedDescription)
}
}
Copy the code
The application receives NSUserActivity from Spotlight
SwiftUI life cycle
.onContinueUserActivity(CSSearchableItemActionType){ userActivity in
if let userinfo = userActivity.userInfo as? [String:Any] {
let identifier = userinfo["kCSSearchableItemActivityIdentifier"] as? String ?? ""
let queryString = userinfo["kCSSearchQueryString"] as? String ?? ""
print(identifier,queryString)
}
}
// Output: starWar
Copy the code
UIKit life cycle
func scene(_ scene: UIScene.continue userActivity: NSUserActivity) {
if userActivity.activityType = = CSSearchableItemActionType {
if let userinfo = userActivity.userInfo as? [String:Any] {
let identifier = userinfo["kCSSearchableItemActivityIdentifier"] as? String ?? ""
let queryString = userinfo["kCSSearchQueryString"] as? String ?? ""
print(identifier,queryString)
}
}
}
Copy the code
Update Spotlight Index
In the same way as adding an index, ensure that the uniqueIdentifier is the same.
let attributeSet = CSSearchableItemAttributeSet(contentType: .text)
attributeSet.displayName = "Star Wars (Modified)"
attributeSet.contentDescription = "Once upon a time, in a galaxy far, far away, jedi knights on a righteous mission fought against the evil dark forces of the empire."
attributeSet.artist = "George Lucas."
let searchableItem = CSSearchableItem(uniqueIdentifier: "starWar", domainIdentifier: "com.fatbobman.Movies.Sci-fi", attributeSet: attributeSet)
CSSearchableIndex.default().indexSearchableItems([searchableItem]){ error in
if let error = error {
print(error.localizedDescription)
}
}
Copy the code
Delete Spotlight index
- Delete the specified
uniqueIdentifier
The project of
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ["starWar"]){ error in
if let error = error {
print(error.localizedDescription)
}
}
Copy the code
- Deletes the item with the specified domain identifier
CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["com.fatbobman.Movies.Sci-fi"]) {_ in }
Copy the code
Deleting a domain identifier is recursive. The above code will only delete all Sci fi groups, while the following code will delete all movie data in the application
CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["com.fatbobman.Movies"]) {_ in }
Copy the code
- Delete all index data in the application
CSSearchableIndex.default().deleteAllSearchableItems{ error in
if let error = error {
print(error.localizedDescription)
}
}
Copy the code
NSCoreDataCoreSpotlightDelegate implementation
NSCoreDataCoreSpotlightDelegate support provides a set of Core Data with Core Spotlight integration method, greatly simplifies the developers to create and maintain the application in the Spotlight in the Core Data, the Data of working hard.
In the WWDC 2021, NSCoreDataCoreSpotlightDelegate further upgrade, through persistent history tracking, developers will not need to manually maintain Data update, delete, Core Data, Data of any changes will be respond in a timely manner in the Spotlight.
Data Model Editor
To index the Core Data in your application in Spotlight, you first need to tag the Entity that you want to index in the Data Model editor.
- Only marked entities can be indexed
- Indexing is triggered only if the attributes of the tagged entity change
For example, if you have several entities created in your application, only the Movie in them is indexed, and the index is updated only when the title and description of the Movie change. Therefore, it is only necessary to enable title and dscription Index in Spotlight in the Movie entity.
Xcode 13 deprecates Store in External Record File and removes setting DisplayName in the Data Model Editor.
NSCoreDataCoreSpotlightDelegate
When is marked entity records Data update (create, modify,), the Core Data will call the attributeSet NSCoreDataCoreSpotlightDelegate method, trying to obtain the corresponding can search term, and update the index.
public class DemoSpotlightDelegate: NSCoreDataCoreSpotlightDelegate {
public override func domainIdentifier(a) -> String {
return "com.fatbobman.CoreSpotlightDemo"
}
public override func attributeSet(for object: NSManagedObject) -> CSSearchableItemAttributeSet? {
if let note = object as? Note {
let attributeSet = CSSearchableItemAttributeSet(contentType: .text)
attributeSet.identifier = "note." + note.viewModel.id.uuidString
attributeSet.displayName = note.viewModel.name
return attributeSet
} else if let item = object as? Item {
let attributeSet = CSSearchableItemAttributeSet(contentType: .text)
attributeSet.identifier = "item." + item.viewModel.id.uuidString
attributeSet.displayName = item.viewModel.name
attributeSet.contentDescription = item.viewModel.descriptioinContent
return attributeSet
}
return nil}}Copy the code
- If you need to index multiple entities in your application, use the
attributeSet
To determine the specific type of managed object, and then create the corresponding searchable item data for it. - Even if a particular piece of data is marked indexable, it can be excluded from the index by returning nil in attributeSet
- Identifiers are best set to identifiers that correspond to your record (identifiers are metadata, not CSSearchableItem)
uniqueIdentifier
), so you can use it directly in later code. - If you do not specify a domain identifier, the default system uses the identifier persisted by Core Data
- When Data records in your app are deleted, Core Data automatically removes searchable items from Spotlight.
CSSearchableItemAttributeSet has many available metadata. For example, you can add thumbnailData or let users dial phoneNUmbers directly from the record (set phoneNUmbers and supportsPhoneCall, respectively). For more information, see the official documentation
CoreDataStack
In the Core Data enable NSCoreDataCoreSpotlightDelegate, there are two prerequisites:
- The persistent store is of type Sqlite
- Persistent History Tracking must be enabled
So in the Core Data Stack you need to use code like this:
class CoreDataStack {
static let shared = CoreDataStack(a)let container: NSPersistentContainer
let spotlightDelegate:NSCoreDataCoreSpotlightDelegate
init(a) {
container = NSPersistentContainer(name: "CoreSpotlightDelegateDemo")
guard let description = container.persistentStoreDescriptions.first else {
fatalError("# # #\(#function): Failed to retrieve a persistent store description.")}// Enable persistent history tracking
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error).\(error.userInfo)")}})// Create index delegate
self.spotlightDelegate = NSCoreDataCoreSpotlightDelegate(forStoreWith: description, coordinator: container.persistentStoreCoordinator)
// Start automatic indexing
spotlightDelegate.startSpotlightIndexing()
}
}
Copy the code
For online application, after adding the function of NSCoreDataCoreSpotlightDelegate, first started, the Core Data will automatically meet the condition (marked) Data is added to the Spotlight index.
In the above code, only persistent history tracking is enabled, and the invalid data is not cleaned regularly, which will lead to data inflation and affect the execution efficiency if it runs for a long time. To learn more about persistent history tracing, read Using Persistent History Tracing in CoreData.
Stop and drop indexes
If you want to rebuild an index, you should stop the index first and then drop it.
stack.spotlightDelegate.stopSpotlightIndexing()
stack.spotlightDelegate.deleteSpotlightIndex{ error in
if let error = error {
print(error)
}
}
Copy the code
Alternatively, you can use the method described above to directly use CSSearchableIndex to more finely delete index content.
onContinueUserActivity
NSCoreDataCoreSpotlight uses the managed object’s URI data as the uniqueIdentifier when creating a CSSearchableItem, so when a user clicks on a search result in Spotlight, We can get this URI from userInfo in the NSUserActivity passed to the application.
Since only a limited amount of information is provided in the NSUserActivity passed to the application (the contentAttributeSet is empty), we can only rely on this URI to determine the corresponding managed object.
SwiftUI provides a convenient method onConinueUserActivity to handle system-passed NSUserActivity.
import SwiftUI
import CoreSpotlight
@main
struct CoreSpotlightDelegateDemoApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.onContinueUserActivity(CSSearchableItemActionType, perform: { na in
if let userinfo = na.userInfo as? [String:Any] {
if let identifier = userinfo["kCSSearchableItemActivityIdentifier"] as? String {
let uri = URL(string:identifier)!
let container = persistenceController.container
if let objectID = container.persistentStoreCoordinator.managedObjectID(forURIRepresentation: uri) {
if let note = container.viewContext.object(with: objectID) as? Note {
// Switch to the corresponding state of note
} else if let item = container.viewContext.object(with: objectID) as? Item {
// Switch to the state corresponding to item}}}}})}}}Copy the code
- Through userinfo
kCSSearchableItemActivityIdentifier
Key to getuniqueIdentifier
(Uri of Core Data) - Convert urIs to NS-managed Bjectid
- The managed object is obtained from objectID
- Set the application to the corresponding state based on the managed object.
I personally don’t like embedding logic for handling NSUserActivity into view code, so if I want to handle NSUserActivity in UIWindowSceneDelegate, See The use of UIWindowSceneDelegate in Core Data with CloudKit (6) for creating applications that share Data with multiple iCloud users.
CSSearchQuery
CoreSpotlight also provides a way to query Spotlight in your application. By creating CSSearchQuery, developers can search Spotlight for the currently indexed data of the application.
func getSearchResult(_ keyword: String) {
let escapedString = keyword.replacingOccurrences(of: "\ \", with: "\ \\ \").replacingOccurrences(of: "\"", with: "\ \\"")
let queryString = "(displayName == \"*" + escapedString + "*\"cd)"
let searchQuery = CSSearchQuery(queryString: queryString, attributes: ["displayName"."contentDescription"])
var spotlightFoundItems = [CSSearchableItem]()
searchQuery.foundItemsHandler = { items in
spotlightFoundItems.append(contentsOf: items)
}
searchQuery.completionHandler = { error in
if let error = error {
print(error.localizedDescription)
}
spotlightFoundItems.forEach { item in
// do something
}
}
searchQuery.start()
}
Copy the code
- The first thing you need to do is to secure the search keyword, right
\
escape queryString
The form of the query is very similar to that of NSPredicate, for example, the above code queries alldisplayName
Contains keyword data (ignoring case and phonetic characters). For more information, seeThe official documentation- Attributes set the attributes required in the returned CSSearchableItem (for example, if there are ten metadata contents in the searchable item, only two of the Settings are returned)
- Called when the search results are obtained
foundItemsHandler
Code in closures - Use after configuration
searchQuery.start()
Start the query
For applications that use Core Data, it may be better to query directly through Core Data.
Matters needing attention
Expiry date
By default, CSSearchableItem has an expirationDate of 30 days. That is, if a piece of data is added to the index, and nothing changes (updates the index) for 30 days, then after 30 days, we won’t be able to search for that data from Spotlight.
There are two solutions:
-
Periodically rebuild the Spotlight index of Core Data
The method is to stop the index – drop the index – restart the index
-
Add expiry date to CSSearchableItemAttributeSet metadata
Under normal circumstances, we can take NSUserActivity set expiry date, and will be CSSearchableItemAttributeSet associated with them. But can only be set in the NSCoreDataCoreSpotlightDelegate CSSearchableItemAttributeSet.
Officials did not publicly CSSearchableItemAttributeSet expiry date attribute, so there is no guarantee that the following method has been effective
if let note = object as? Note {
let attributeSet = CSSearchableItemAttributeSet(contentType: .text)
attributeSet.identifier = "note." + note.viewModel.id.uuidString
attributeSet.displayName = note.viewModel.name
attributeSet.setValue(Date.distantFuture, forKey: "expirationDate")
return attributeSet
}
Copy the code
The setValue will automatically CSSearchableItemAttributeSet _kMDItemExpirationDate set to 4001-01-01, Spotlight will set _kMDItemExpirationDate to the expirationDate of NSUserActivity
Fuzzy query
Spotlight supports fuzzy queries. Typing xingqiu, for example, might bring up “Star Wars” like the one above. However, Apple does not have the ability to open up fuzzy queries in CSSearchQuery. If you want a Spotlight experience in your app, you’re better off creating your own code in Core Data.
Also, Spotlight’s fuzzy query only works for displayName, not contentDescription
Space constraints
CSSearchableItemAttributeSet the metadata is used to describe the record, are not suitable for save a large amount of data. ContentDescription currently supports a maximum of 300 characters. If you have a lot of content, it’s best to capture information that is really useful to your users.
Number of searchable items
The number of searchable items in your app should be limited to a few thousand. Beyond this magnitude, query performance will be severely affected
conclusion
Hopefully, more apps will recognize the importance of Spotlight and make it an important portal for devices as soon as possible.
Hope you found this article helpful.
This article originally appeared on my blog [Elbow’s Swift Notepad]
Welcome to subscribe to my public number: Elbow Swift Notepad
Other recommendations:
SheetKit — SwiftUI Modal View Extension Library
How to implement in SwiftUI interactiveDismissDisabled
Core Data with CloudKit 1-6
How to preview SwiftUI view with Core Data elements in Xcode
www.fatbobman.com/posts/uikit…
Enhance SwiftUI’s navigation view with NavigationViewKit
@ AppStorage research