axiom-core-spotlight-ref

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Core Spotlight & NSUserActivity Reference

Core Spotlight & NSUserActivity 参考指南

Overview

概述

Comprehensive guide to Core Spotlight framework and NSUserActivity for making app content discoverable in Spotlight search, enabling Siri predictions, and supporting Handoff. Core Spotlight directly indexes app content while NSUserActivity captures user engagement for prediction.
Key distinction Core Spotlight = indexing all app content; NSUserActivity = marking current user activity for prediction/handoff.

本指南全面介绍Core Spotlight框架与NSUserActivity,帮助你让应用内容在Spotlight搜索中可被发现、启用Siri预测并支持接力功能。Core Spotlight直接索引应用内容,而NSUserActivity则捕捉用户的操作行为用于预测功能。
核心区别 Core Spotlight = 索引所有应用内容;NSUserActivity = 标记当前用户操作以实现预测/接力功能。

When to Use This Skill

何时使用本技能

Use this skill when:
  • Indexing app content (documents, notes, orders, messages) for Spotlight
  • Using NSUserActivity for Handoff or Siri predictions
  • Choosing between CSSearchableItem, IndexedEntity, and NSUserActivity
  • Implementing activity continuation from Spotlight results
  • Batch indexing for performance
  • Deleting indexed content
  • Debugging Spotlight search not finding app content
  • Integrating NSUserActivity with App Intents (appEntityIdentifier)
Do NOT use this skill for:
  • App Shortcuts implementation (use app-shortcuts-ref)
  • App Intents basics (use app-intents-ref)
  • Overall discoverability strategy (use app-discoverability)

在以下场景中使用本技能:
  • 为Spotlight搜索索引应用内容(文档、笔记、订单、消息等)
  • 使用NSUserActivity实现接力或Siri预测功能
  • 在CSSearchableItem、IndexedEntity与NSUserActivity之间做选择
  • 实现从Spotlight搜索结果中继续操作的功能
  • 为提升性能进行批量索引
  • 删除已索引的内容
  • 调试Spotlight搜索无法找到应用内容的问题
  • 将NSUserActivity与App Intents集成(使用appEntityIdentifier)
请勿在以下场景使用本技能:
  • 实现App Shortcuts(请使用app-shortcuts-ref)
  • App Intents基础使用(请使用app-intents-ref)
  • 整体可发现性策略制定(请使用app-discoverability)

Related Skills

相关技能

  • app-intents-ref — App Intents framework including IndexedEntity
  • app-discoverability — Strategic guide for making apps discoverable
  • app-shortcuts-ref — App Shortcuts for instant availability

  • app-intents-ref — 包含IndexedEntity的App Intents框架
  • app-discoverability — 应用可发现性策略指南
  • app-shortcuts-ref — 实现即时可用的App Shortcuts

When to Use Each API

各API的适用场景

Use CaseApproachExample
User viewing specific screen
NSUserActivity
User opened order details
Index all app content
CSSearchableItem
All 500 orders searchable
App Intents entity search
IndexedEntity
"Find orders where..."
Handoff between devices
NSUserActivity
Continue editing note on Mac
Background content indexing
CSSearchableItem
batch
Index documents on launch
Apple guidance Use NSUserActivity for user-initiated activities (screens currently visible), not as a general indexing mechanism. For comprehensive content indexing, use Core Spotlight's CSSearchableItem.

使用场景实现方式示例
用户查看特定页面
NSUserActivity
用户打开订单详情页
索引所有应用内容
CSSearchableItem
让全部500条订单可被搜索
App Intents实体搜索
IndexedEntity
"查找符合...条件的订单"
设备间接力
NSUserActivity
在Mac上继续编辑笔记
后台内容索引
CSSearchableItem
批量处理
应用启动时索引文档
苹果官方建议 将NSUserActivity用于用户主动发起的操作(当前可见页面),而非作为通用索引机制。如需全面索引内容,请使用Core Spotlight的CSSearchableItem。

Core Spotlight (CSSearchableItem)

Core Spotlight (CSSearchableItem)

Creating Searchable Items

创建可搜索项

swift
import CoreSpotlight
import UniformTypeIdentifiers

func indexOrder(_ order: Order) {
    // 1. Create attribute set with metadata
    let attributes = CSSearchableItemAttributeSet(contentType: .item)
    attributes.title = order.coffeeName
    attributes.contentDescription = "Ordered on \(order.date.formatted())"
    attributes.keywords = ["coffee", "order", order.coffeeName.lowercased()]
    attributes.thumbnailData = order.imageData

    // Optional: Add location
    attributes.latitude = order.location.coordinate.latitude
    attributes.longitude = order.location.coordinate.longitude

    // Optional: Add rating
    attributes.rating = NSNumber(value: order.rating)

    // 2. Create searchable item
    let item = CSSearchableItem(
        uniqueIdentifier: order.id.uuidString,        // Stable ID
        domainIdentifier: "orders",                   // Grouping
        attributeSet: attributes
    )

    // Optional: Set expiration
    item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)  // 1 year

    // 3. Index the item
    CSSearchableIndex.default().indexSearchableItems([item]) { error in
        if let error = error {
            print("Indexing error: \(error.localizedDescription)")
        }
    }
}

swift
import CoreSpotlight
import UniformTypeIdentifiers

func indexOrder(_ order: Order) {
    // 1. 创建包含元数据的属性集
    let attributes = CSSearchableItemAttributeSet(contentType: .item)
    attributes.title = order.coffeeName
    attributes.contentDescription = "下单时间:\(order.date.formatted())"
    attributes.keywords = ["coffee", "order", order.coffeeName.lowercased()]
    attributes.thumbnailData = order.imageData

    // 可选:添加位置信息
    attributes.latitude = order.location.coordinate.latitude
    attributes.longitude = order.location.coordinate.longitude

    // 可选:添加评分
    attributes.rating = NSNumber(value: order.rating)

    // 2. 创建可搜索项
    let item = CSSearchableItem(
        uniqueIdentifier: order.id.uuidString,        // 稳定ID
        domainIdentifier: "orders",                   // 分组标识
        attributeSet: attributes
    )

    // 可选:设置过期时间
    item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)  // 1年

    // 3. 索引该项目
    CSSearchableIndex.default().indexSearchableItems([item]) { error in
        if let error = error {
            print("索引错误:\(error.localizedDescription)")
        }
    }
}

Key Properties

关键属性

uniqueIdentifier

uniqueIdentifier

Purpose Stable, persistent ID unique to this item within your app.
swift
uniqueIdentifier: order.id.uuidString
Requirements:
  • Must be stable (same item = same identifier)
  • Used for updates and deletion
  • Scoped to your app

用途 应用内该项目独有的稳定、持久ID。
swift
uniqueIdentifier: order.id.uuidString
要求:
  • 必须稳定(同一项目对应同一标识符)
  • 用于更新和删除操作
  • 作用域限定在当前应用内

domainIdentifier

domainIdentifier

Purpose Groups related items for bulk operations.
swift
domainIdentifier: "orders"
Use cases:
  • Delete all items in a domain
  • Organize by type (orders, documents, messages)
  • Batch operations
Pattern:
swift
// Index with domains
item1.domainIdentifier = "orders"
item2.domainIdentifier = "documents"

// Delete entire domain
CSSearchableIndex.default().deleteSearchableItems(
    withDomainIdentifiers: ["orders"]
) { error in }

用途 将相关项目分组,以便批量操作。
swift
domainIdentifier: "orders"
适用场景:
  • 删除某个域下的所有项目
  • 按类型分类(订单、文档、消息)
  • 批量操作
示例模式:
swift
// 按域索引
item1.domainIdentifier = "orders"
item2.domainIdentifier = "documents"

// 删除整个域的内容
CSSearchableIndex.default().deleteSearchableItems(
    withDomainIdentifiers: ["orders"]
) { error in }

CSSearchableItemAttributeSet

CSSearchableItemAttributeSet

Metadata describing the searchable content.
swift
let attributes = CSSearchableItemAttributeSet(contentType: .item)

// Required
attributes.title = "Order #1234"
attributes.displayName = "Coffee Order"

// Highly recommended
attributes.contentDescription = "Medium latte with oat milk"
attributes.keywords = ["coffee", "latte", "order"]
attributes.thumbnailData = imageData

// Optional but valuable
attributes.contentCreationDate = Date()
attributes.contentModificationDate = Date()
attributes.rating = NSNumber(value: 5)
attributes.comment = "My favorite order"
用于描述可搜索内容的元数据。
swift
let attributes = CSSearchableItemAttributeSet(contentType: .item)

// 必填项
attributes.title = "订单 #1234"
attributes.displayName = "咖啡订单"

// 强烈推荐设置
attributes.contentDescription = "中杯燕麦拿铁"
attributes.keywords = ["coffee", "latte", "order"]
attributes.thumbnailData = imageData

// 可选但有价值的属性
attributes.contentCreationDate = Date()
attributes.contentModificationDate = Date()
attributes.rating = NSNumber(value: 5)
attributes.comment = "我最爱的订单"

Common Attributes

常用属性

AttributePurposeExample
title
Primary title"Coffee Order #1234"
displayName
User-visible name"Morning Latte"
contentDescription
Description text"Medium latte with oat milk"
keywords
Search terms["coffee", "latte"]
thumbnailData
Preview imageJPEG/PNG data
contentCreationDate
When createdDate()
contentModificationDate
Last modifiedDate()
rating
Star ratingNSNumber(value: 5)
latitude
/
longitude
Location37.7749, -122.4194
属性用途示例
title
主标题"咖啡订单 #1234"
displayName
用户可见名称"晨间拿铁"
contentDescription
描述文本"中杯燕麦拿铁"
keywords
搜索关键词["coffee", "latte"]
thumbnailData
预览图片JPEG/PNG数据
contentCreationDate
创建时间Date()
contentModificationDate
最后修改时间Date()
rating
星级评分NSNumber(value: 5)
latitude
/
longitude
位置信息37.7749, -122.4194

Document-Specific Attributes

文档专属属性

swift
// For document types
attributes.contentType = UTType.pdf
attributes.author = "John Doe"
attributes.pageCount = 10
attributes.fileSize = 1024000
attributes.path = "/path/to/document.pdf"
swift
// 针对文档类型
attributes.contentType = UTType.pdf
attributes.author = "John Doe"
attributes.pageCount = 10
attributes.fileSize = 1024000
attributes.path = "/path/to/document.pdf"

Message-Specific Attributes

消息专属属性

swift
// For messages
attributes.recipients = ["jane@example.com"]
attributes.recipientNames = ["Jane Doe"]
attributes.authorNames = ["John Doe"]
attributes.subject = "Meeting notes"

swift
// 针对消息类型
attributes.recipients = ["jane@example.com"]
attributes.recipientNames = ["Jane Doe"]
attributes.authorNames = ["John Doe"]
attributes.subject = "会议纪要"

Batch Indexing for Performance

批量索引以提升性能

❌ DON'T: Index items one at a time

❌ 错误做法:逐个索引项目

swift
// Bad: 100 index operations
for order in orders {
    CSSearchableIndex.default().indexSearchableItems([order.asSearchableItem()]) { _ in }
}
swift
// 不佳:执行100次索引操作
for order in orders {
    CSSearchableIndex.default().indexSearchableItems([order.asSearchableItem()]) { _ in }
}

✅ DO: Batch index operations

✅ 正确做法:批量索引操作

swift
// Good: 1 index operation
let items = orders.map { $0.asSearchableItem() }

CSSearchableIndex.default().indexSearchableItems(items) { error in
    if let error = error {
        print("Batch indexing error: \(error)")
    } else {
        print("Indexed \(items.count) items")
    }
}
Recommended batch size 100-500 items per call. For larger sets, split into multiple batches.

swift
// 推荐:执行1次索引操作
let items = orders.map { $0.asSearchableItem() }

CSSearchableIndex.default().indexSearchableItems(items) { error in
    if let error = error {
        print("批量索引错误:\(error)")
    } else {
        print("已索引\(items.count)个项目")
    }
}
推荐批量大小 每次调用处理100-500个项目。对于更大的数据集,拆分为多个批次处理。

Deletion Patterns

删除模式

Delete by Identifier

按标识符删除

swift
let identifiers = ["order-1", "order-2", "order-3"]

CSSearchableIndex.default().deleteSearchableItems(
    withIdentifiers: identifiers
) { error in
    if let error = error {
        print("Deletion error: \(error)")
    }
}
swift
let identifiers = ["order-1", "order-2", "order-3"]

CSSearchableIndex.default().deleteSearchableItems(
    withIdentifiers: identifiers
) { error in
    if let error = error {
        print("删除错误:\(error)")
    }
}

Delete by Domain

按域删除

swift
// Delete all items in "orders" domain
CSSearchableIndex.default().deleteSearchableItems(
    withDomainIdentifiers: ["orders"]
) { error in }
swift
// 删除"orders"域下的所有项目
CSSearchableIndex.default().deleteSearchableItems(
    withDomainIdentifiers: ["orders"]
) { error in }

Delete All

删除全部内容

swift
// Nuclear option: delete everything
CSSearchableIndex.default().deleteAllSearchableItems { error in
    if let error = error {
        print("Failed to delete all: \(error)")
    }
}
When to delete:
  • User deletes content
  • Content expires
  • User logs out
  • App reset/reinstall

swift
// 彻底删除:清除所有索引内容
CSSearchableIndex.default().deleteAllSearchableItems { error in
    if let error = error {
        print("删除全部内容失败:\(error)")
    }
}
删除时机:
  • 用户删除内容时
  • 内容过期时
  • 用户登出时
  • 应用重置/重新安装时

App Entity Integration (App Intents)

应用实体集成(App Intents)

Create from App Entity

从应用实体创建

swift
import AppIntents

struct OrderEntity: AppEntity, IndexedEntity {
    var id: UUID

    @Property(title: "Coffee", indexingKey: \.title)
    var coffeeName: String

    @Property(title: "Date", indexingKey: \.contentCreationDate)
    var orderDate: Date

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Order"

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(coffeeName)", subtitle: "Order from \(orderDate.formatted())")
    }
}

// Create searchable item from entity
let order = OrderEntity(id: UUID(), coffeeName: "Latte", orderDate: Date())
let item = CSSearchableItem(appEntity: order)
CSSearchableIndex.default().indexSearchableItems([item])
swift
import AppIntents

struct OrderEntity: AppEntity, IndexedEntity {
    var id: UUID

    @Property(title: "Coffee", indexingKey: \.title)
    var coffeeName: String

    @Property(title: "Date", indexingKey: \.contentCreationDate)
    var orderDate: Date

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Order"

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(coffeeName)", subtitle: "下单时间:\(orderDate.formatted())")
    }
}

// 从实体创建可搜索项
let order = OrderEntity(id: UUID(), coffeeName: "Latte", orderDate: Date())
let item = CSSearchableItem(appEntity: order)
CSSearchableIndex.default().indexSearchableItems([item])

Associate Entity with Existing Item

将实体与现有项目关联

swift
let attributes = CSSearchableItemAttributeSet(contentType: .item)
attributes.title = "Order #1234"

let item = CSSearchableItem(
    uniqueIdentifier: "order-1234",
    domainIdentifier: "orders",
    attributeSet: attributes
)

// Associate with App Intent entity
item.associateAppEntity(orderEntity, priority: .default)
Benefits:
  • Automatic "Find" actions in Shortcuts
  • Spotlight search returns entities directly
  • App Intents integration

swift
let attributes = CSSearchableItemAttributeSet(contentType: .item)
attributes.title = "订单 #1234"

let item = CSSearchableItem(
    uniqueIdentifier: "order-1234",
    domainIdentifier: "orders",
    attributeSet: attributes
)

// 关联到App Intent实体
item.associateAppEntity(orderEntity, priority: .default)
优势:
  • 在快捷指令中自动启用"查找"操作
  • Spotlight搜索直接返回实体
  • 与App Intents深度集成

NSUserActivity

NSUserActivity

Overview

概述

NSUserActivity captures user engagement for:
  • Handoff — Continue activity on another device
  • Spotlight search — Index currently viewed content
  • Siri predictions — Suggest returning to this screen
  • Quick Note — Link notes to app content
Platform support iOS 8.0+, iPadOS 8.0+, macOS 10.10+, tvOS 9.0+, watchOS 2.0+, axiom-visionOS 1.0+

NSUserActivity用于捕捉用户操作行为,支持:
  • 接力 — 在另一台设备上继续当前操作
  • Spotlight搜索 — 索引当前查看的内容
  • Siri预测 — 建议用户返回该页面
  • 快速笔记 — 将笔记链接到应用内容
平台支持 iOS 8.0+, iPadOS 8.0+, macOS 10.10+, tvOS 9.0+, watchOS 2.0+, visionOS 1.0+

Eligibility Properties

eligibility属性

swift
let activity = NSUserActivity(activityType: "com.app.viewOrder")

// Enable Spotlight search
activity.isEligibleForSearch = true

// Enable Siri predictions
activity.isEligibleForPrediction = true

// Enable Handoff to other devices
activity.isEligibleForHandoff = true

// Contribute URL to global search (public content only)
activity.isEligibleForPublicIndexing = false
Privacy note Only set
isEligibleForPublicIndexing = true
for publicly accessible content (e.g., blog posts with public URLs).

swift
let activity = NSUserActivity(activityType: "com.app.viewOrder")

// 启用Spotlight搜索
activity.isEligibleForSearch = true

// 启用Siri预测
activity.isEligibleForPrediction = true

// 启用设备间接力
activity.isEligibleForHandoff = true

// 向全局搜索贡献URL(仅适用于公开内容)
activity.isEligibleForPublicIndexing = false
隐私提示 仅当内容可公开访问时(如带有公共URL的博客文章),才设置
isEligibleForPublicIndexing = true

Basic Pattern

基础使用模式

swift
func viewOrder(_ order: Order) {
    // 1. Create activity
    let activity = NSUserActivity(activityType: "com.coffeeapp.viewOrder")
    activity.title = order.coffeeName

    // 2. Set eligibility
    activity.isEligibleForSearch = true
    activity.isEligibleForPrediction = true

    // 3. Provide identifier for updates/deletion
    activity.persistentIdentifier = order.id.uuidString

    // 4. Provide rich metadata
    let attributes = CSSearchableItemAttributeSet(contentType: .item)
    attributes.title = order.coffeeName
    attributes.contentDescription = "Your \(order.coffeeName) order"
    attributes.thumbnailData = order.imageData
    activity.contentAttributeSet = attributes

    // 5. Mark as current
    activity.becomeCurrent()

    // 6. Store reference (important!)
    self.userActivity = activity
}
Critical Maintain strong reference to activity. It won't appear in search without one.

swift
func viewOrder(_ order: Order) {
    // 1. 创建活动
    let activity = NSUserActivity(activityType: "com.coffeeapp.viewOrder")
    activity.title = order.coffeeName

    // 2. 设置可用权限
    activity.isEligibleForSearch = true
    activity.isEligibleForPrediction = true

    // 3. 提供用于更新/删除的标识符
    activity.persistentIdentifier = order.id.uuidString

    // 4. 提供丰富元数据
    let attributes = CSSearchableItemAttributeSet(contentType: .item)
    attributes.title = order.coffeeName
    attributes.contentDescription = "你的\(order.coffeeName)订单"
    attributes.thumbnailData = order.imageData
    activity.contentAttributeSet = attributes

    // 5. 标记为当前活动
    activity.becomeCurrent()

    // 6. 保存引用(非常重要!)
    self.userActivity = activity
}
关键注意事项 必须保持对活动的强引用,否则它不会出现在搜索结果中。

becomeCurrent() and resignCurrent()

becomeCurrent() 和 resignCurrent()

swift
// UIKit pattern
class OrderDetailViewController: UIViewController {
    var currentActivity: NSUserActivity?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let activity = NSUserActivity(activityType: "com.app.viewOrder")
        activity.title = order.coffeeName
        activity.isEligibleForSearch = true
        activity.becomeCurrent()  // Mark as active

        self.currentActivity = activity
        self.userActivity = activity  // UIKit integration
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        currentActivity?.resignCurrent()  // Mark as inactive
    }
}
swift
// SwiftUI pattern
struct OrderDetailView: View {
    let order: Order

    var body: some View {
        VStack {
            Text(order.coffeeName)
        }
        .onAppear {
            let activity = NSUserActivity(activityType: "com.app.viewOrder")
            activity.title = order.coffeeName
            activity.isEligibleForSearch = true
            activity.becomeCurrent()

            // SwiftUI automatically manages userActivity
            self.userActivity = activity
        }
    }
}

swift
// UIKit 模式
class OrderDetailViewController: UIViewController {
    var currentActivity: NSUserActivity?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let activity = NSUserActivity(activityType: "com.app.viewOrder")
        activity.title = order.coffeeName
        activity.isEligibleForSearch = true
        activity.becomeCurrent()  // 标记为活动状态

        self.currentActivity = activity
        self.userActivity = activity  // UIKit 集成
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        currentActivity?.resignCurrent()  // 标记为非活动状态
    }
}
swift
// SwiftUI 模式
struct OrderDetailView: View {
    let order: Order

    var body: some View {
        VStack {
            Text(order.coffeeName)
        }
        .onAppear {
            let activity = NSUserActivity(activityType: "com.app.viewOrder")
            activity.title = order.coffeeName
            activity.isEligibleForSearch = true
            activity.becomeCurrent()

            // SwiftUI 自动管理userActivity
            self.userActivity = activity
        }
    }
}

App Intents Integration (appEntityIdentifier)

App Intents集成(appEntityIdentifier)

Connect NSUserActivity to App Intent entities.
swift
func viewOrder(_ order: Order) {
    let activity = NSUserActivity(activityType: "com.app.viewOrder")
    activity.title = order.coffeeName
    activity.isEligibleForSearch = true
    activity.isEligibleForPrediction = true

    // Connect to App Intent entity
    activity.appEntityIdentifier = order.id.uuidString

    // Now Spotlight can surface this as an entity suggestion
    activity.becomeCurrent()
    self.userActivity = activity
}
Benefits:
  • Siri suggests this order in relevant contexts
  • App Intents can reference this activity
  • Shortcuts integration

将NSUserActivity与App Intent实体关联:
swift
func viewOrder(_ order: Order) {
    let activity = NSUserActivity(activityType: "com.app.viewOrder")
    activity.title = order.coffeeName
    activity.isEligibleForSearch = true
    activity.isEligibleForPrediction = true

    // 关联到App Intent实体
    activity.appEntityIdentifier = order.id.uuidString

    // 现在Spotlight可以将此作为实体建议展示
    activity.becomeCurrent()
    self.userActivity = activity
}
优势:
  • Siri会在相关场景下推荐该订单
  • App Intents可以引用此活动
  • 与快捷指令集成

On-Screen Content Tagging

屏幕内容标记

Pattern from WWDC Tag currently visible content for Spotlight parameter suggestions.
swift
func showEvent(_ event: Event) {
    let activity = NSUserActivity(activityType: "com.app.viewEvent")
    activity.persistentIdentifier = event.id.uuidString

    // Spotlight suggests this event for intent parameters
    activity.appEntityIdentifier = event.id.uuidString

    activity.becomeCurrent()
    userActivity = activity
}
Result When users invoke intents requiring an event parameter, Spotlight suggests the currently visible event.

WWDC推荐模式 标记当前可见内容,用于Spotlight参数建议。
swift
func showEvent(_ event: Event) {
    let activity = NSUserActivity(activityType: "com.app.viewEvent")
    activity.persistentIdentifier = event.id.uuidString

    // Spotlight会在意图参数中推荐此活动
    activity.appEntityIdentifier = event.id.uuidString

    activity.becomeCurrent()
    userActivity = activity
}
效果 当用户调用需要事件参数的意图时,Spotlight会推荐当前可见的事件。

Quick Note Integration (macOS/iPadOS)

快速笔记集成(macOS/iPadOS)

For Quick Note linking, activities must:
  1. Be the app's current activity (via
    becomeCurrent()
    )
  2. Have a clear, concise
    title
    (nouns, not verbs)
  3. Provide stable, consistent identifiers
  4. Support navigation to linked content indefinitely
  5. Gracefully handle missing content
swift
let activity = NSUserActivity(activityType: "com.app.viewNote")
activity.title = note.title  // ✅ "Project Ideas" not ❌ "View Note"
activity.persistentIdentifier = note.id.uuidString
activity.targetContentIdentifier = note.id.uuidString
activity.becomeCurrent()

如需支持快速笔记链接,活动必须满足:
  1. 是应用的当前活动(通过
    becomeCurrent()
    设置)
  2. 拥有清晰简洁的
    title
    (使用名词而非动词)
  3. 提供稳定一致的标识符
  4. 支持长期导航到链接内容
  5. 优雅处理内容缺失的情况
swift
let activity = NSUserActivity(activityType: "com.app.viewNote")
activity.title = note.title  // ✅ "项目想法" 而非 ❌ "查看笔记"
activity.persistentIdentifier = note.id.uuidString
activity.targetContentIdentifier = note.id.uuidString
activity.becomeCurrent()

Activity Continuation (Handling Spotlight Taps)

活动续接(处理Spotlight点击)

When users tap Spotlight results, handle continuation:
当用户点击Spotlight搜索结果时,处理续接逻辑:

UIKit

UIKit

swift
// AppDelegate or SceneDelegate
func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    guard userActivity.activityType == "com.app.viewOrder" else {
        return false
    }

    // Extract identifier
    if let identifier = userActivity.persistentIdentifier,
       let orderID = UUID(uuidString: identifier) {
        // Navigate to order
        navigateToOrder(orderID)
        return true
    }

    return false
}
swift
// AppDelegate 或 SceneDelegate
func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    guard userActivity.activityType == "com.app.viewOrder" else {
        return false
    }

    // 提取标识符
    if let identifier = userActivity.persistentIdentifier,
       let orderID = UUID(uuidString: identifier) {
        // 导航到订单页面
        navigateToOrder(orderID)
        return true
    }

    return false
}

SwiftUI

SwiftUI

swift
@main
struct CoffeeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onContinueUserActivity("com.app.viewOrder") { userActivity in
                    if let identifier = userActivity.persistentIdentifier,
                       let orderID = UUID(uuidString: identifier) {
                        // Navigate to order
                        navigateToOrder(orderID)
                    }
                }
        }
    }
}
swift
@main
struct CoffeeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onContinueUserActivity("com.app.viewOrder") { userActivity in
                    if let identifier = userActivity.persistentIdentifier,
                       let orderID = UUID(uuidString: identifier) {
                        // 导航到订单页面
                        navigateToOrder(orderID)
                    }
                }
        }
    }
}

Searchable Item Continuation

可搜索项续接

swift
// When continuing from CSSearchableItem
func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    if userActivity.activityType == CSSearchableItemActionType {
        // Get identifier from Core Spotlight item
        if let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
            // Navigate based on identifier
            navigateToItem(identifier)
            return true
        }
    }

    return false
}

swift
// 从Core Spotlight项续接
func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    if userActivity.activityType == CSSearchableItemActionType {
        // 从Core Spotlight项获取标识符
        if let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
            // 根据标识符导航
            navigateToItem(identifier)
            return true
        }
    }

    return false
}

Deletion APIs

删除API

Delete All Saved Activities

删除所有已保存活动

swift
NSUserActivity.deleteAllSavedUserActivities { }
swift
NSUserActivity.deleteAllSavedUserActivities { }

Delete Specific Activities

删除特定活动

swift
let identifiers = ["order-1", "order-2"]

NSUserActivity.deleteSavedUserActivities(
    withPersistentIdentifiers: identifiers
) { }
When to delete:
  • User deletes content
  • User logs out
  • Content no longer accessible

swift
let identifiers = ["order-1", "order-2"]

NSUserActivity.deleteSavedUserActivities(
    withPersistentIdentifiers: identifiers
) { }
删除时机:
  • 用户删除内容时
  • 用户登出时
  • 内容不再可访问时

NSUserActivity vs CSSearchableItem

NSUserActivity vs CSSearchableItem

AspectNSUserActivityCSSearchableItem
PurposeCurrent user activityIndexing all content
When to useUser viewing a screenBackground content indexing
ScopeOne item at a timeBatch operations
HandoffSupportedNot supported
PredictionSupportedNot supported
SearchLimitedFull Spotlight integration
ExampleUser viewing order detailIndex all 500 orders
Recommended Use both:
  • NSUserActivity for screens currently visible
  • CSSearchableItem for comprehensive content indexing

维度NSUserActivityCSSearchableItem
用途当前用户操作索引所有内容
使用场景用户查看页面时后台内容索引时
范围一次处理一个项目批量操作
接力功能支持不支持
预测功能支持不支持
搜索能力有限完整Spotlight集成
示例用户查看订单详情索引全部500条订单
推荐方案 同时使用两者:
  • NSUserActivity用于当前可见页面
  • CSSearchableItem用于全面内容索引

Testing & Debugging

测试与调试

Verify Indexed Items

验证已索引项目

Using Spotlight

使用Spotlight

  1. Open Spotlight (swipe down on Home Screen)
  2. Search for indexed content keywords
  3. Verify your app's results appear
  4. Tap result → Verify navigation works
  1. 打开Spotlight(在主屏幕向下滑动)
  2. 搜索已索引内容的关键词
  3. 验证应用的搜索结果是否显示
  4. 点击结果 → 验证导航是否正常

Using Console Logs

使用控制台日志

swift
CSSearchableIndex.default().fetchLastClientState { clientState, error in
    if let error = error {
        print("Error fetching client state: \(error)")
    } else {
        print("Client state: \(clientState?.base64EncodedString() ?? "none")")
    }
}

swift
CSSearchableIndex.default().fetchLastClientState { clientState, error in
    if let error = error {
        print("获取客户端状态错误:\(error)")
    } else {
        print("客户端状态:\(clientState?.base64EncodedString() ?? "无")")
    }
}

Common Issues

常见问题

Items not appearing in Spotlight

项目未出现在Spotlight中

  • Wait 1-2 minutes for indexing
  • Verify
    isEligibleForSearch = true
  • Check System Settings → Siri & Search → [App] → Show App in Search
  • Restart device
  • Check console for indexing errors
  • 等待1-2分钟完成索引
  • 验证
    isEligibleForSearch = true
    已设置
  • 检查系统设置 → Siri与搜索 → [你的应用] → 显示应用在搜索结果中
  • 重启设备
  • 检查控制台的索引错误日志

Activity not triggering Handoff

活动无法触发接力

  • Verify
    isEligibleForHandoff = true
  • Ensure both devices signed into same iCloud account
  • Check Bluetooth and Wi-Fi enabled on both devices
  • Verify activityType is reverse DNS (com.company.app.action)
  • 验证
    isEligibleForHandoff = true
    已设置
  • 确保两台设备登录同一iCloud账户
  • 检查两台设备的蓝牙和Wi-Fi已启用
  • 验证activityType为反向DNS格式(如com.company.app.action)

Continuation not working

续接功能无效

  • Verify
    application(_:continue:restorationHandler:)
    implemented
  • Check activityType matches exactly
  • Ensure persistentIdentifier is set
  • Test with debugger to verify method is called

  • 确认已实现
    application(_:continue:restorationHandler:)
    方法
  • 检查activityType完全匹配
  • 确保已设置persistentIdentifier
  • 使用调试器验证方法是否被调用

Best Practices

最佳实践

1. Selective Indexing

1. 选择性索引

❌ DON'T: Index everything

❌ 错误做法:索引所有内容

swift
// Bad: Index all 10,000 items
let allItems = try await ItemService.shared.all()
swift
// 不佳:索引全部10,000个项目
let allItems = try await ItemService.shared.all()

✅ DO: Index selectively

✅ 正确做法:选择性索引

swift
// Good: Index recent/important items
let recentItems = try await ItemService.shared.recent(limit: 100)
let favoriteItems = try await ItemService.shared.favorites()
Why Performance, quota limits, user experience.

swift
// 推荐:索引近期/重要项目
let recentItems = try await ItemService.shared.recent(limit: 100)
let favoriteItems = try await ItemService.shared.favorites()
原因 提升性能、避免配额限制、优化用户体验。

2. Use Domain Identifiers

2. 使用域标识符

❌ DON'T: Rely only on unique identifiers

❌ 错误做法:仅依赖唯一标识符

swift
// Hard to delete all orders
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: allOrderIDs)
swift
// 难以删除所有订单
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: allOrderIDs)

✅ DO: Group with domains

✅ 正确做法:按域分组

swift
// Easy to delete all orders
CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["orders"])

swift
// 轻松删除所有订单
CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["orders"])

3. Set Expiration Dates

3. 设置过期时间

❌ DON'T: Index items forever

❌ 错误做法:永久索引项目

swift
// Bad: Items never expire
let item = CSSearchableItem(/* ... */)
swift
// 不佳:项目永不失效
let item = CSSearchableItem(/* ... */)

✅ DO: Set reasonable expiration

✅ 正确做法:设置合理的过期时间

swift
// Good: Expire after 1 year
item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)

swift
// 推荐:1年后过期
item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)

4. Provide Rich Metadata

4. 提供丰富元数据

❌ DON'T: Minimal metadata

❌ 错误做法:仅提供最少元数据

swift
attributes.title = "Item"
swift
attributes.title = "Item"

✅ DO: Rich, searchable metadata

✅ 正确做法:提供丰富的可搜索元数据

swift
attributes.title = "Medium Latte Order"
attributes.contentDescription = "Ordered on December 12, 2025"
attributes.keywords = ["coffee", "latte", "order", "medium"]
attributes.thumbnailData = imageData

swift
attributes.title = "中杯拿铁订单"
attributes.contentDescription = "下单时间:2025年12月12日"
attributes.keywords = ["coffee", "latte", "order", "medium"]
attributes.thumbnailData = imageData

5. Handle Missing Content Gracefully

5. 优雅处理内容缺失

swift
func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    guard let identifier = userActivity.persistentIdentifier else {
        return false
    }

    // Attempt to load content
    if let item = try? await ItemService.shared.fetch(id: identifier) {
        navigate(to: item)
        return true
    } else {
        // Content deleted or unavailable
        showAlert("This content is no longer available")

        // Delete activity from search
        NSUserActivity.deleteSavedUserActivities(
            withPersistentIdentifiers: [identifier]
        )

        return true  // Still handled
    }
}

swift
func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    guard let identifier = userActivity.persistentIdentifier else {
        return false
    }

    // 尝试加载内容
    if let item = try? await ItemService.shared.fetch(id: identifier) {
        navigate(to: item)
        return true
    } else {
        // 内容已删除或不可用
        showAlert("该内容已无法访问")

        // 从搜索中删除该活动
        NSUserActivity.deleteSavedUserActivities(
            withPersistentIdentifiers: [identifier]
        )

        return true  // 仍标记为已处理
    }
}

Complete Example

完整示例

Comprehensive Integration

综合集成

swift
import CoreSpotlight
import UniformTypeIdentifiers

class OrderManager {

    // MARK: - Core Spotlight Indexing

    func indexOrder(_ order: Order) {
        let attributes = CSSearchableItemAttributeSet(contentType: .item)
        attributes.title = order.coffeeName
        attributes.contentDescription = "Order from \(order.date.formatted())"
        attributes.keywords = ["coffee", "order", order.coffeeName.lowercased()]
        attributes.thumbnailData = order.thumbnailImageData
        attributes.contentCreationDate = order.date
        attributes.rating = NSNumber(value: order.rating)

        let item = CSSearchableItem(
            uniqueIdentifier: order.id.uuidString,
            domainIdentifier: "orders",
            attributeSet: attributes
        )

        item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)

        CSSearchableIndex.default().indexSearchableItems([item]) { error in
            if let error = error {
                print("Indexing error: \(error)")
            }
        }
    }

    func deleteOrder(_ orderID: UUID) {
        // Delete from Core Spotlight
        CSSearchableIndex.default().deleteSearchableItems(
            withIdentifiers: [orderID.uuidString]
        )

        // Delete NSUserActivity
        NSUserActivity.deleteSavedUserActivities(
            withPersistentIdentifiers: [orderID.uuidString]
        )
    }

    func deleteAllOrders() {
        CSSearchableIndex.default().deleteSearchableItems(
            withDomainIdentifiers: ["orders"]
        )
    }

    // MARK: - NSUserActivity for Current Screen

    func createActivityForOrder(_ order: Order) -> NSUserActivity {
        let activity = NSUserActivity(activityType: "com.coffeeapp.viewOrder")
        activity.title = order.coffeeName
        activity.isEligibleForSearch = true
        activity.isEligibleForPrediction = true
        activity.persistentIdentifier = order.id.uuidString

        // Connect to App Intents
        activity.appEntityIdentifier = order.id.uuidString

        // Rich metadata
        let attributes = CSSearchableItemAttributeSet(contentType: .item)
        attributes.title = order.coffeeName
        attributes.contentDescription = "Your \(order.coffeeName) order"
        attributes.thumbnailData = order.thumbnailImageData
        activity.contentAttributeSet = attributes

        return activity
    }
}

// UIKit view controller
class OrderDetailViewController: UIViewController {
    var order: Order!
    var currentActivity: NSUserActivity?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        currentActivity = OrderManager.shared.createActivityForOrder(order)
        currentActivity?.becomeCurrent()
        self.userActivity = currentActivity
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        currentActivity?.resignCurrent()
    }
}

// SwiftUI view
struct OrderDetailView: View {
    let order: Order

    var body: some View {
        VStack {
            Text(order.coffeeName)
                .font(.largeTitle)

            Text("Ordered on \(order.date.formatted())")
                .foregroundColor(.secondary)
        }
        .userActivity("com.coffeeapp.viewOrder") { activity in
            activity.title = order.coffeeName
            activity.isEligibleForSearch = true
            activity.isEligibleForPrediction = true
            activity.persistentIdentifier = order.id.uuidString
            activity.appEntityIdentifier = order.id.uuidString

            let attributes = CSSearchableItemAttributeSet(contentType: .item)
            attributes.title = order.coffeeName
            attributes.contentDescription = "Your \(order.coffeeName) order"
            activity.contentAttributeSet = attributes
        }
    }
}

swift
import CoreSpotlight
import UniformTypeIdentifiers

class OrderManager {

    // MARK: - Core Spotlight 索引

    func indexOrder(_ order: Order) {
        let attributes = CSSearchableItemAttributeSet(contentType: .item)
        attributes.title = order.coffeeName
        attributes.contentDescription = "下单时间:\(order.date.formatted())"
        attributes.keywords = ["coffee", "order", order.coffeeName.lowercased()]
        attributes.thumbnailData = order.thumbnailImageData
        attributes.contentCreationDate = order.date
        attributes.rating = NSNumber(value: order.rating)

        let item = CSSearchableItem(
            uniqueIdentifier: order.id.uuidString,
            domainIdentifier: "orders",
            attributeSet: attributes
        )

        item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)

        CSSearchableIndex.default().indexSearchableItems([item]) { error in
            if let error = error {
                print("索引错误:\(error)")
            }
        }
    }

    func deleteOrder(_ orderID: UUID) {
        // 从Core Spotlight删除
        CSSearchableIndex.default().deleteSearchableItems(
            withIdentifiers: [orderID.uuidString]
        )

        // 删除NSUserActivity
        NSUserActivity.deleteSavedUserActivities(
            withPersistentIdentifiers: [orderID.uuidString]
        )
    }

    func deleteAllOrders() {
        CSSearchableIndex.default().deleteSearchableItems(
            withDomainIdentifiers: ["orders"]
        )
    }

    // MARK: - NSUserActivity 用于当前页面

    func createActivityForOrder(_ order: Order) -> NSUserActivity {
        let activity = NSUserActivity(activityType: "com.coffeeapp.viewOrder")
        activity.title = order.coffeeName
        activity.isEligibleForSearch = true
        activity.isEligibleForPrediction = true
        activity.persistentIdentifier = order.id.uuidString

        // 关联到App Intents
        activity.appEntityIdentifier = order.id.uuidString

        // 丰富元数据
        let attributes = CSSearchableItemAttributeSet(contentType: .item)
        attributes.title = order.coffeeName
        attributes.contentDescription = "你的\(order.coffeeName)订单"
        attributes.thumbnailData = order.thumbnailImageData
        activity.contentAttributeSet = attributes

        return activity
    }
}

// UIKit 视图控制器
class OrderDetailViewController: UIViewController {
    var order: Order!
    var currentActivity: NSUserActivity?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        currentActivity = OrderManager.shared.createActivityForOrder(order)
        currentActivity?.becomeCurrent()
        self.userActivity = currentActivity
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        currentActivity?.resignCurrent()
    }
}

// SwiftUI 视图
struct OrderDetailView: View {
    let order: Order

    var body: some View {
        VStack {
            Text(order.coffeeName)
                .font(.largeTitle)

            Text("下单时间:\(order.date.formatted())")
                .foregroundColor(.secondary)
        }
        .userActivity("com.coffeeapp.viewOrder") { activity in
            activity.title = order.coffeeName
            activity.isEligibleForSearch = true
            activity.isEligibleForPrediction = true
            activity.persistentIdentifier = order.id.uuidString
            activity.appEntityIdentifier = order.id.uuidString

            let attributes = CSSearchableItemAttributeSet(contentType: .item)
            attributes.title = order.coffeeName
            attributes.contentDescription = "你的\(order.coffeeName)订单"
            activity.contentAttributeSet = attributes
        }
    }
}

Resources

资源

WWDC: 260, 2015-709
Docs: /corespotlight, /corespotlight/cssearchableitem, /foundation/nsuseractivity
Skills: axiom-app-intents-ref, axiom-app-discoverability, axiom-app-shortcuts-ref

Remember Core Spotlight indexes all your app's content; NSUserActivity marks what the user is currently doing. Use CSSearchableItem for batch indexing, NSUserActivity for active screens, and connect them to App Intents with appEntityIdentifier for comprehensive discoverability.
WWDC: 260, 2015-709
文档: /corespotlight, /corespotlight/cssearchableitem, /foundation/nsuseractivity
相关技能: axiom-app-intents-ref, axiom-app-discoverability, axiom-app-shortcuts-ref

记住 Core Spotlight用于索引应用的所有内容;NSUserActivity用于标记用户当前的操作。使用CSSearchableItem进行批量索引,使用NSUserActivity处理当前可见页面,并通过appEntityIdentifier将它们与App Intents关联,以实现全面的可发现性。