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 specifieduniqueIdentifierThe 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 theattributeSetTo 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 userinfokCSSearchableItemActivityIdentifierKey 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
  • queryStringThe form of the query is very similar to that of NSPredicate, for example, the above code queries alldisplayNameContains 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 obtainedfoundItemsHandlerCode in closures
  • Use after configurationsearchQuery.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