Set Up Core Spotlight with Core Data: Getting Started

Learn how to connect Core Data with Core Spotlight and add search capability to your app using Spotlight. By Warren Burton.

4.7 (6) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

Searching Inside the App

You’ve done all this work to provide search results for when people are searching outside your app. Now, you’ll learn how to leverage Spotlight inside your app. In this section, you’ll add a search feature that looks up results in the Spotlight index.

View of the bug

The following sections will show you how to do just that.

Searching Core Spotlight

First, you’ll add the machinery to search the Spotlight index. In the Project navigator, within the group Controller, open BugController.swift. Then, add this import to the top of the file:

import CoreSpotlight

Next, add these properties to BugController:

private var searchQuery: CSSearchQuery?
private var spotlightFoundItems: [CSSearchableItem] = []

Then, add the following extension to the end of the file:

extension BugController {
  private func fetchSearchResults(_ items: [CSSearchableItem]) {
    let foundBugs = items.compactMap { (item: CSSearchableItem) -> CDBug? in
      guard let bugURI = URL(string: item.uniqueIdentifier) else {
        return nil
      }
      return dataStack.bugWithURI(bugURI)
    }
    bugs = foundBugs
  }
}

Spotlight returns instances of CSSearchableItem. In this method, you map the uniqueIdentifier of CSSearchableItem — which is a Core Data object URI — to the corresponding CDBug instance. At the end, you update bugs with the result to update the list in the UI.

The next step is to create a CSSearchQuery that will perform the search for you. Add this method to the same extension:

private func searchCoreSpotlight(_ term: String) {
  // 1
  let escapedTerm = term
    .replacingOccurrences(of: "\\", with: "\\\\")
    .replacingOccurrences(of: "\"", with: "\\\"")
  let queryString = "(textContent == \"\(escapedTerm)*\"cd)"
  
  // 2
  searchQuery = CSSearchQuery(
    queryString: queryString,
    attributes: ["textContent"])

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

  // 4
  searchQuery?.completionHandler = { error in
    guard error == nil else {
      print(error?.localizedDescription ?? "oh no!")
      return
    }

    DispatchQueue.main.async {
      self.fetchSearchResults(self.spotlightFoundItems)
      self.spotlightFoundItems.removeAll()
    }
  }

  // 5
  searchQuery?.start()
}

In this method you set up and start a CSSearchQuery based on your search term:

  1. Sanitize your search term, and then construct a query string. The syntax for this string is documented here. The query used here says “Search the textContent attribute with a case and diacritic insensitive search”.
  2. Instantiate CSSearchQuery with that query string and a list of the attributes from CSSearchableItemAttributeSet that you want to search.
  3. Append found results to an array, as the foundItemsHandler of CSSearchQuery may be called many times during a search.
  4. The completionHandler of CSSearchQuery is only called once. For a happy path, use fetchSearchResults() to convert the results to records in the PointyBug database and clean up.
  5. Call start() to trigger the query.

The last step is to provide access to the search function. Add this final method to the same extension:

func setSearchText(_ term: String) {
  guard !term.isEmpty else {
    searchQuery?.cancel()
    bugs = fetchedResults.fetchedObjects ?? []
    return
  }
  searchCoreSpotlight(term)
}

This method either resets the bug list to an unfiltered state or runs a search. You’re all done with the machinery. Now, all you need to do is add some UI.

Searchable SwiftUI

SwiftUI search is very simple. You declare an element as searchable, and SwiftUI interprets that as needed.

In the Project navigator, in the group Views, open BugListView.swift. Add this property to the top of BugListView:

@State var searchText = ""

With this, you declare a bindable @State property to act as storage for the search term.

Next, within the body of BugListView, add the following modifiers to the List at the marker // add searchable here:

.searchable(text: $searchText)
.onChange(of: searchText) { newValue in
  bugController.setSearchText(newValue)
}

You can’t help but love SwiftUI for syntax like this. You declare the List as searchable and bind to searchText. The final modifier, onChange, relays the search text to the method you created on BugController.

Build and run. Then, pull down on the main list to reveal the search bar, and search for QA:

Filtered bugs

You can see all the bugs that are in a QA state.

Congratulations! You’ve created a fully featured search experience for your app. Quit your simulator to give the fans a rest.

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

In this tutorial, you learned all the steps necessary to use Core Spotlight with a Core Data database, including how to:

  • Upgrade your Core Data model to support Core Spotlight.
  • Create a custom NSCoreDataCoreSpotlightDelegate to connect your database to CoreSpotlight.
  • Start and stop the indexer when needed.
  • Use CoreSpotlight for searching within your app.

Hopefully, this tutorial has demonstrated the simplicity and the power of the new API included with iOS 15 and macOS 12, and perhaps it’ll inspire you to both use Core Data and expose your data to Spotlight in clever ways.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!