CoreSpotlight for content indexing, NSUserActivity for handoff and search, and making app content discoverable. Use when user asks about Spotlight search, CoreSpotlight, NSUserActivity, content indexing, or app discoverability.
/plugin marketplace add bluewaves-creations/bluewaves-skills/plugin install swift-apple-dev@bluewaves-skillsThis skill is limited to using the following tools:
Comprehensive guide to CoreSpotlight for content indexing, NSUserActivity for handoff and search, and making your app's content discoverable in iOS 26.
| Feature | CoreSpotlight | NSUserActivity |
|---|---|---|
| Timing | Any time | When user views content |
| Scope | All content | Viewed content |
| Handoff | No | Yes |
| Web indexing | No | Yes |
| Ranking | Default | Usage-boosted |
import CoreSpotlight
import MobileCoreServices
import CoreSpotlight
func indexNote(_ note: Note) {
// Create searchable item attributes
let attributes = CSSearchableItemAttributeSet(contentType: .text)
attributes.title = note.title
attributes.contentDescription = note.content
attributes.lastUsedDate = note.modifiedAt
attributes.keywords = note.tags
// Optional: thumbnail
if let thumbnailData = note.thumbnailData {
attributes.thumbnailData = thumbnailData
}
// Create searchable item
let item = CSSearchableItem(
uniqueIdentifier: note.id.uuidString,
domainIdentifier: "com.yourapp.notes",
attributeSet: attributes
)
// Optional: Set expiration
item.expirationDate = Date().addingTimeInterval(30 * 24 * 60 * 60) // 30 days
// Index the item
CSSearchableIndex.default().indexSearchableItems([item]) { error in
if let error {
print("Indexing failed: \(error)")
}
}
}
func indexNote(_ note: Note) async throws {
let attributes = CSSearchableItemAttributeSet(contentType: .text)
attributes.title = note.title
attributes.contentDescription = note.content
let item = CSSearchableItem(
uniqueIdentifier: note.id.uuidString,
domainIdentifier: "com.yourapp.notes",
attributeSet: attributes
)
try await CSSearchableIndex.default().indexSearchableItems([item])
}
func indexAllNotes(_ notes: [Note]) async throws {
let items = notes.map { note -> CSSearchableItem in
let attributes = CSSearchableItemAttributeSet(contentType: .text)
attributes.title = note.title
attributes.contentDescription = note.content
attributes.lastUsedDate = note.modifiedAt
return CSSearchableItem(
uniqueIdentifier: note.id.uuidString,
domainIdentifier: "com.yourapp.notes",
attributeSet: attributes
)
}
try await CSSearchableIndex.default().indexSearchableItems(items)
}
func createRichAttributes(for note: Note) -> CSSearchableItemAttributeSet {
let attributes = CSSearchableItemAttributeSet(contentType: .text)
// Basic info
attributes.title = note.title
attributes.contentDescription = note.content
attributes.displayName = note.title
// Dates
attributes.contentCreationDate = note.createdAt
attributes.contentModificationDate = note.modifiedAt
attributes.lastUsedDate = note.lastViewedAt
// Keywords and categorization
attributes.keywords = note.tags
attributes.subject = note.category
// Media (if applicable)
if let imageData = note.thumbnailData {
attributes.thumbnailData = imageData
}
// Contact info (for contact-related content)
attributes.authorNames = [note.author]
attributes.authorEmailAddresses = [note.authorEmail]
// Location (if applicable)
if let location = note.location {
attributes.latitude = location.latitude as NSNumber
attributes.longitude = location.longitude as NSNumber
attributes.namedLocation = location.name
}
// Custom attributes
attributes.identifier = note.id.uuidString
attributes.relatedUniqueIdentifier = note.folder?.id.uuidString
return attributes
}
// Delete specific item
func deleteFromIndex(noteId: UUID) async throws {
try await CSSearchableIndex.default().deleteSearchableItems(
withIdentifiers: [noteId.uuidString]
)
}
// Delete by domain
func deleteAllNotes() async throws {
try await CSSearchableIndex.default().deleteSearchableItems(
withDomainIdentifiers: ["com.yourapp.notes"]
)
}
// Delete all indexed content
func deleteAllIndexedContent() async throws {
try await CSSearchableIndex.default().deleteAllSearchableItems()
}
func updateNoteInIndex(_ note: Note) async throws {
// Simply re-index with same identifier
// CoreSpotlight replaces existing item
try await indexNote(note)
}
import UIKit
class NoteViewController: UIViewController {
var note: Note!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupUserActivity()
}
func setupUserActivity() {
let activity = NSUserActivity(activityType: "com.yourapp.viewNote")
// Basic properties
activity.title = note.title
activity.userInfo = ["noteId": note.id.uuidString]
// Enable features
activity.isEligibleForSearch = true // Spotlight search
activity.isEligibleForPrediction = true // Siri suggestions
activity.isEligibleForHandoff = true // Handoff to other devices
// Search attributes
let attributes = CSSearchableItemAttributeSet(contentType: .text)
attributes.title = note.title
attributes.contentDescription = note.content
activity.contentAttributeSet = attributes
// Keywords
activity.keywords = Set(note.tags)
// Associate with view controller
userActivity = activity
activity.becomeCurrent()
}
}
struct NoteDetailView: View {
let note: Note
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text(note.title)
.font(.largeTitle)
Text(note.content)
}
}
.userActivity("com.yourapp.viewNote") { activity in
activity.title = note.title
activity.userInfo = ["noteId": note.id.uuidString]
activity.isEligibleForSearch = true
activity.isEligibleForHandoff = true
let attributes = CSSearchableItemAttributeSet(contentType: .text)
attributes.title = note.title
attributes.contentDescription = note.content
activity.contentAttributeSet = attributes
}
}
}
func setupWebEligibleActivity() {
let activity = NSUserActivity(activityType: "com.yourapp.viewArticle")
activity.title = article.title
activity.webpageURL = URL(string: "https://yourapp.com/articles/\(article.id)")
activity.isEligibleForSearch = true
activity.isEligibleForPublicIndexing = true // Can appear in public search
activity.isEligibleForHandoff = true
userActivity = activity
activity.becomeCurrent()
}
// Use same identifier in both
let uniqueId = note.id.uuidString
// CoreSpotlight item
let spotlightItem = CSSearchableItem(
uniqueIdentifier: uniqueId,
domainIdentifier: "com.yourapp.notes",
attributeSet: attributes
)
// NSUserActivity
let activity = NSUserActivity(activityType: "com.yourapp.viewNote")
activity.contentAttributeSet?.relatedUniqueIdentifier = uniqueId
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
// Handle NSUserActivity
if userActivity.activityType == "com.yourapp.viewNote" {
if let noteId = userActivity.userInfo?["noteId"] as? String {
navigateToNote(id: noteId)
return true
}
}
// Handle CoreSpotlight result
if userActivity.activityType == CSSearchableItemActionType {
if let uniqueId = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
navigateToNote(id: uniqueId)
return true
}
}
return false
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(
_ scene: UIScene,
continue userActivity: NSUserActivity
) {
handleUserActivity(userActivity)
}
func handleUserActivity(_ activity: NSUserActivity) {
switch activity.activityType {
case "com.yourapp.viewNote":
if let noteId = activity.userInfo?["noteId"] as? String {
navigateToNote(id: noteId)
}
case CSSearchableItemActionType:
if let uniqueId = activity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
navigateToNote(id: uniqueId)
}
default:
break
}
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onContinueUserActivity("com.yourapp.viewNote") { activity in
handleNoteActivity(activity)
}
.onContinueUserActivity(CSSearchableItemActionType) { activity in
handleSpotlightActivity(activity)
}
}
}
func handleNoteActivity(_ activity: NSUserActivity) {
guard let noteId = activity.userInfo?["noteId"] as? String else { return }
// Navigate to note
router.navigateTo(.note(id: noteId))
}
func handleSpotlightActivity(_ activity: NSUserActivity) {
guard let uniqueId = activity.userInfo?[CSSearchableItemActivityIdentifier] as? String else { return }
router.navigateTo(.note(id: uniqueId))
}
}
func checkIndexStatus() async throws {
let index = CSSearchableIndex.default()
// Check if indexing is enabled
let status = try await index.fetchLastClientState()
print("Last indexed: \(status)")
}
class IndexManager {
func performIncrementalUpdate() async throws {
let index = CSSearchableIndex.default()
// Get last sync state
let lastState = try await index.fetchLastClientState()
// Fetch changes since last state
let changes = try await fetchChangesSince(lastState)
// Index new/modified items
let newItems = changes.added + changes.modified
if !newItems.isEmpty {
let searchableItems = newItems.map { createSearchableItem(for: $0) }
try await index.indexSearchableItems(searchableItems)
}
// Delete removed items
if !changes.deleted.isEmpty {
try await index.deleteSearchableItems(withIdentifiers: changes.deleted)
}
// Save new state
let newState = createCurrentState()
try await index.beginBatch()
try await index.endBatch(withClientState: newState)
}
}
import CoreSpotlight
class SpotlightDelegate: NSObject, CSSearchableIndexDelegate {
func searchableIndex(
_ searchableIndex: CSSearchableIndex,
reindexAllSearchableItemsWithAcknowledgementHandler acknowledgementHandler: @escaping () -> Void
) {
// System requested full reindex
Task {
try? await reindexAllContent()
acknowledgementHandler()
}
}
func searchableIndex(
_ searchableIndex: CSSearchableIndex,
reindexSearchableItemsWithIdentifiers identifiers: [String],
acknowledgementHandler: @escaping () -> Void
) {
// System requested reindex of specific items
Task {
try? await reindexItems(withIds: identifiers)
acknowledgementHandler()
}
}
func data(for searchableIndex: CSSearchableIndex, itemIdentifier: String, typeIdentifier: String) throws -> Data {
// Provide data for a searchable item (e.g., for preview)
guard let note = NoteManager.shared.find(id: itemIdentifier) else {
throw IndexError.itemNotFound
}
return note.content.data(using: .utf8) ?? Data()
}
func fileURL(for searchableIndex: CSSearchableIndex, itemIdentifier: String, typeIdentifier: String, inPlace: Bool) throws -> URL {
// Provide file URL for item
throw IndexError.notSupported
}
}
@main
struct MyApp: App {
let spotlightDelegate = SpotlightDelegate()
init() {
CSSearchableIndex.default().indexDelegate = spotlightDelegate
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class NoteManager {
func save(_ note: Note) async throws {
try await database.save(note)
// Index immediately after save
try? await SpotlightIndexer.shared.index(note)
}
func delete(_ note: Note) async throws {
// Remove from index first
try? await SpotlightIndexer.shared.removeFromIndex(note.id)
try await database.delete(note)
}
}
// Short-lived content
item.expirationDate = Date().addingTimeInterval(7 * 24 * 60 * 60) // 7 days
// Long-lived content
item.expirationDate = Date().addingTimeInterval(365 * 24 * 60 * 60) // 1 year
// Never expire
item.expirationDate = nil
// Group related content
CSSearchableItem(
uniqueIdentifier: note.id.uuidString,
domainIdentifier: "com.yourapp.notes", // Easy bulk operations
attributeSet: attributes
)
// Delete all notes at once
try await index.deleteSearchableItems(withDomainIdentifiers: ["com.yourapp.notes"])
// Include all relevant attributes
attributes.title = note.title
attributes.contentDescription = note.content
attributes.keywords = note.tags
attributes.lastUsedDate = note.lastViewedAt
attributes.thumbnailData = note.thumbnail
// Better search relevance
func safeIndex(_ note: Note) async {
do {
try await indexNote(note)
} catch {
// Log but don't crash
logger.error("Failed to index note: \(error)")
}
}
// In simulator/device:
// 1. Index content
// 2. Pull down on home screen
// 3. Search for indexed content
// 4. Tap result to verify deep linking
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.