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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Set Up Core Spotlight with Core Data: Getting Started
30 mins
- Getting Started
- Adding Spotlight to Your Core Data Model
- Configuring Spotlight in Your Persistent Store
- Creating a Spotlight Delegate
- Connecting a Spotlight Delegate
- Describing Searchable Items
- Trying It Out
- Heavy Lifting
- Building a Bridge
- Importing From an API
- Running the Fetch
- Opening a Search Result in Your App
- Including Image Thumbnails with Spotlight
- Deleting Spotlight Data
- Searching Inside the App
- Searching Core Spotlight
- Searchable SwiftUI
- Where to Go From Here?
Describing Searchable Items
To describe a searchable item, use a CSSearchableItemAttributeSet
from CoreSpotlight
.
Open BugSpotlightDelegate.swift and add the code below to BugSpotlightDelegate
:
override func attributeSet(for object: NSManagedObject) -> CSSearchableItemAttributeSet? { guard let bug = object as? CDBug else { return nil } let attributeSet = CSSearchableItemAttributeSet(contentType: .text) let base = bug.textValue let tags = (bug.tags as? Set<CDTag> ?? []) .compactMap { $0.name } .joined(separator: " ") let idString = "PB \(String(format: "%03d", bug.bugID))" attributeSet.textContent = idString + base + " " + tags attributeSet.displayName = "\(idString): \(bug.primaryTag?.name ?? "")" attributeSet.contentDescription = base return attributeSet }
attributeSet(for:)
is the method that Core Spotlight calls when it’s indexing. BugSpotlightDelegate
calls this method for each NSManagedObject
that needs indexing.
In this code, you perform the following actions:
- Cast the
object
asCDBug
. - Create a
CSSearchableItemAttributeSet
of typetext
. - Set the searchable text in
textContent
. - Set the
displayName
andcontentDescription
that will appear in Spotlight when you search.
Notice how textContent
and displayName
don’t need to be the same thing. The set of strings that leads your customer to this result can be different than what’s displayed in the UI.
This only touches the surface of the CSSearchableItemAttributeSet
API. Read the documentation to determine how to improve search results.
Trying It Out
Now, you’re ready to do some spotlighting. Build and run. When the app finishes launching, press Command-Shift-H to return to the Home Screen. Drag down in the center of your iPad to expose Spotlight. Now, search for button.
You’ll need to scroll down the page and maybe tap “Show More” to see the result, but there it is. Your app data is showing up in the system search:
Tap the result and notice it opens PointyBug, but nothing’s shown. Soon, you’ll find out how to respond to the open event and complete the circle by selecting the result in PointyBug. But first, you need to find out when to stop and start the indexer.
Heavy Lifting
Many apps use REST API endpoints to populate their local databases. You don’t want Spotlight to be indexing while you import the data, as you want all the CPU on parsing and importing.
In this section, you’ll import some data to PointyBug and wrap that call in a stop and start sequence.
Building a Bridge
You’ll be working with a fictional API. The data actually comes from a file in the project, demodata.tsv. The code to read the data from this file is in NetworkController.swift, but for now, you’ll stay focused on the search.
In the Project navigator, open CDBug+Help.swift in the group Core Data. Add this extension to the end of the file:
extension CDBug { static func fromRemoteRepresentation( _ remote: RemoteBug, in context: NSManagedObjectContext ) -> CDBug { let bug = createOrFetchExisting(bugID: remote.bugID, in: context) bug.text = remote.text if let tagname = remote.tagID, let tag = Tag.tagNamed(tagname, in: context) { bug.addToTags(tag) } return bug } }
This code converts RemoteBug
to a Core Data object, CDBug
. This pattern of bridging remote data shields your inner database from changes in the remote API at the cost of some boilerplate code. Notice the code uses the helper method, createOrFetchExisting(bugID:in:)
, to fetch an existing bug if it exists. This prevents creating duplicate bugs when syncing with the server.
Next, in Controller, open BugController.swift. Add this code to the end of the file:
extension BugController { private func makeBugs( with remotes: [RemoteBug], completion: @escaping () -> Void ) { // 1 let worker = dataStack.makeWorkerContext() // 2 worker.perform { var index: Int16 = 10000 // 3 _ = remotes.map { rBug in let dbBug = CDBug.fromRemoteRepresentation(rBug, in: worker) dbBug.orderingIndex = index index += 1 } // 4 try? worker.save() DispatchQueue.main.async { completion() } } } }
The pattern you see is frequently used when working with Core Data and imported data:
- Create a worker context. This is a
NSManagedObjectContext
, which is a child of the main queue context and has its ownDispatchQueue
. - Tell the worker to perform work in its own queue asynchronously.
- Each
RemoteBug
maps to aCDBug
and gets a large ordering index to force the bug to the end of the list. You don’t need to keep the mapped array, as the context holds the new records. - Save the worker context, which merges these changes back to the main queue context and your UI.
By doing hard work in its own queue, you don’t risk locking up the user UI.
Importing From an API
Next, you’ll use this setup code to fetch the remote API. Add this method to the same extension
in BugController
:
func syncWithServer() throws { // 1 let bugfetcher: BugFetchProtocol = NetworkController() // 2 dataStack.toggleSpotlightIndexing(enabled: false) try bugfetcher.fetchBugs { result in switch result { case .success(let remoteBugs): makeBugs(with: remoteBugs) { [self] in assertOrderingIndex() // 3 dataStack.saveContext() // 4 dataStack.toggleSpotlightIndexing(enabled: true) } case .failure(let error): print("oh no! - the remote fetch failed -\(error.localizedDescription)") dataStack.toggleSpotlightIndexing(enabled: true) } } }
In this method, you deal with starting and stopping Spotlight indexing while the app does the hard work:
- Create an object that conforms to
BugFetchProtocol
. By using a protocol interface,BugController
doesn’t care who fetches bugs. This pattern improves testability. - Turn off Spotlight indexing.
- When
makeBugs(with:completion:)
completes, save the main context, moving the imported bugs all the way to theNSPersistentStore
. - Turn indexing back on. Spotlight will figure out what has changed and get to work on indexing the
NSPersistentStore
.
You now need to make a couple of small changes to your Core Data system to help with the data import.
Running the Fetch
The last thing you need to do is make the call to the API. You’ll use a button to trigger this event.
In the Project navigator, in the group Views, open BugListView.swift. Inside an HStack
, near the bottom of the view, locate the comment // Insert first button here
. Add this code at that mark to declare a button:
Button("SYNC") { try? bugController.syncWithServer() } .padding(8) .foregroundColor(.white) .background(Color.orange) .cornerRadius(10, antialiased: true)
The action for the button calls syncWithServer
, which you defined in the previous section. Build and run. You’ll see a shiny new button at the bottom of the main bug list:
Tap the SYNC button and some new bugs will appear in the list:
You can synchronize as many times as you like, but the bug list will only change once. Press Command-Shift-H to trigger a save.
All this work for a few records might seem like overkill, but consider if you have an API returning 1,000 records to your app. Even the latest iPad Pro would take time to import all that data.
Your bug list now has a few items in it, so it’s time to figure out how to use a Spotlight search result to open a corresponding bug in PointyBug.