大多数浏览器和
Developer App 均支持流媒体播放。
-
在聚焦中展示 app 数据
了解 Core Data 如何通过短短两行代码在“聚焦”中显示您的 App 中的数据。了解如何让“聚焦”搜索可以发现这些数据,以及如何自定义这些数据在设备上的显示方式。最后,我们将展示如何在您的 App 中完全以“聚焦”索引的数据驱动来实现全文搜索。
资源
相关视频
WWDC21
-
下载
♪低音音乐播放♪ ♪ 大卫史戴兹:嗨 欢迎收看 《在聚焦中展示应用程序数据》 我是大卫史戴兹 我是Core Data团队的工程师 在这场课程 我很期待向你示范 如何用 NSCoreDataCoreSpotlightDelegate 在你的app中添加“聚焦”的索引 这场课程的议程首先开始了解 NSCoreDataCore SpotlightDelegate对象 然后是为什么你应该用它 设置一个简单的实现 了解如何自定义那个实现 最后 通过添加全文检索验证代码 首先 我们来看Core Data和聚焦 大家会在你的应用程序中 创建并存储很多很棒、很重要的内容 随着对你的app的用量 和数据集的规模扩大 他们会想要能够快速找到数据 无论是在应用程序中用标准搜索方法 或是在应用程序之外 例如 用聚焦搜索 你的应用程序中的数据 能在聚焦出现的话是不是很棒? 没错 这就是Core Data 可以帮忙的地方 NSCoreData CoreSpotlightDelegate对象 会负责所有繁重的部分 并且提供一组API 快速有效率地 索引你的应用程序提供的内容 你只需要打开它! 一旦索引完成 搜索结果也会出现在 应用程序外的聚焦搜索用户界面 聚焦委托会自动处理 你的图形的托管对象的改变 然后相应地更新聚焦索引 此外 它提供稳健的索引管理功能 能和私人、仅限设备上的索引交互 并让你能根据喜好定制索引结果 事实上你的永久性存储中的任何内容 都可以被索引 使用聚焦委托的原因有三 一 聚焦委托会维持 和Core Spotlight API的功能一致性 二 它移除很多必要实现代码 三 它提供一个很棒的额外功能集 我们晚点在这场课程会谈到 为了描绘我先前的论点 这是个用Core Spotlight API 一个很简单的实现 这些API只添加项目到搜索索引 并将它减少成…这样! 两行! 简单 容易阅读和维持 毕竟谁不喜欢较少行代码? 我们来看如何设置并马上开始运行 这个简单范例会包含 决定为哪些东西编制索引 并创建委托 这场课程中 我会一直提到 一个叫做标记的应用程序 是我为自己写的 它是个简单的照片标记应用程序 这个范例应用程序会结合很多 我今天要谈的API 添加聚焦支持之前 可以看到所有标记和照片数据被困在 标记里面 因为搜索 “Natural Bridges State Park” 并没有出现聚焦搜索查询结果 我们来改变这一点 NSCoreData CoreSpotlightDelegate的 任何实现的第一步 是决定你要在聚焦中索引什么 什么东西会在聚焦中索引 完全由你决定 在标记中 我决定索引 Photo实体上的 userSpecifiedName特性 和Tag实体上的名称特性 要让模型准备好进行索引 我在Xcode中打开了项目的 Core Data模型 选定每个我想索引的特性 并勾选特性检查器中的 聚焦中的索引复选框 我们在Core Data模型编辑器中 还有工作要做 因为我们必须设置 Core Data聚焦显示名称 Core Data聚焦显示名称 是一个NSExpression 编制索引时 这个表达式会和每个 有由聚焦索引的属性的托管对象 一起评估 然后结果会被存储 之后 显示聚焦搜索用户界面时 这些存储结果会用来作为 搜索结果的显示名称 什么是NSExpression? 这个嘛 表达式可以很简单 像是评估键路径 在这例子中是Tag.name 不过除了评估键路径以外 这个对象还有好几招 这个例子中 它为你算数学 表达式甚至可以更复杂 例如计算一组数字的 标准差 标记中 聚焦显示名称 设置为Photo实体上的 userSpecifiedName 和Tag实体上的Name 现在模型已经做好索引的准备 我们来创建聚焦委托吧 从iOS 15和macOS Monterey开始 初始值设定项forStoreWith: model 现在已弃用 初始化聚焦委托的新方式 是用forStoreWith:coordinator: 通过采用新的指定初始值设定项 你不再需要先添加聚焦委托实例 到存储选项 才能将存储添加到协调器 不过 你必须调用 startSpotlightIndexing 聚焦委托才能开始工作 我想要强调几个 NSCoreData CoreSpotlightDelegate的需求 索引存储的存储类型必须是SQLite 而且必须启用永久性历史纪录跟踪 这样就完成了! 就这样! 不需做其他动作 你的数据就能在聚焦中索引 我刚示范了添加聚焦索引 到我的标记应用程序有多简单 现在我已经描述了基本部分 我们来稍加自定义这个实现 第一个自定义实现的方式 是定义一个域和索引名称 首先 我会定义一个类 TagsSpotlightDelegate 这是 NSCoreData CoreSpotlightDelegate的子类 现在 我会用一个实现替代 domainName和indexName 替代这些选择器告诉聚焦 要在哪里存储 索引数据 并让你之后能更好识别它 特别是如果你有多个索引 如果你不替代domainIdentifier 默认域标识符是存储标识符 如果你不替代indexName 默认索引名称是nil 自定义聚焦委托的下一步 是定义一个特性集 在本讲座的设置部分 NSCoreDataCore SpotlightDelegate对象 为我们定义了特性集“返回聚焦” 只需勾选聚焦中的索引复选框 现在我要示范究竟如何指定 用来编制索引的特性 指定哪些特性应该被索引 让你能更明确地控制什么东西被索引 还有它如何被搜索 要做到这点 用CSSearchableItemAttributeSet 一个特性集包含数个预定义属性 让你能指定托管对象 以搜索结果出现时 要显示的元数据 你选择的特性完全取决于你的域 你可以用预定义属性 那些在CSSearchableItem AttributeSet中可以找到 或是你可以定义自己的属性 标记应用程序使用预定义属性 关键词、displayName 和thumbnailData 有一点很重要 一次只能修改 一个线程上的一个特性集 因为并发访问特性集中的属性 有未定义行为 回到TagsSpotlightDelegate类 我们看看它如何运作 通过替代 attributeSet (for object:) 在替代实现中 首先确定 对象是否为Photo类型对象 接着 用.image内容类型 初始化一个attributeSet 然后用来自Photo对象的适合特性 设置特性集上的 属性标识符、displayName 和thumbnailData 现在 将Photo对象标记集的标记 附加到特性集上的关键词数组 此时值得一提的是 如果你的模型索引一个关系 attributeSet (for object:) 必须被替代 才能定义关系的哪个特定部分 要被索引 最后 返回特性集 因为模型也索引Tag对象 代码需要处理标记的案例 要做到那点 创建一个 .text内容类型的特性集 将显示名称设置为标记的名称 然后返回特性集 最后一步 移除前一步骤在模型编辑器中设置的 Core Data聚焦显示名称 我们进一步为开始和停止索引 定义一个事件循环 之前 我们设置聚焦委托时 startSpotlightIndexing 在创建聚焦委托后 马上被调用 为了让你能精准控制 NSCoreDataCoreSpotlightDelegate 何时执行索引工作 stopSpotlightIndexing 也被添加到框架 协同使用这两个选择器让你能够 视需求开始和停止索引工作 例如你的应用程序在执行激烈的CPU 或磁盘活动运算时 现在我们来添加一些支持 让我们在索引更新完成时 能收到通知 当聚焦中索引的 一个或多个实体发生变化 该索引会异步更新 在iOS 15和macOS Monterey中 Core Data框架 添加了索引更新通知 要在索引更新完成时收到通知 NSCoreDataCoreSpotlightDelegate 订阅.indexDidUpdateNotification 它是由聚焦委托发布 这些通知会在 处理存储NSManagedObjectContext 调用后 或在批处理操作完成后发布 我们看看它的实际运作 首先 检查是否已启用索引 若已启用 登记 indexDidUpdateNotification 接着在处理程序中检视通知 它会有个userInfo字典 里面包含两个键值对 类似远程变更通知 一个是存储的NSString UUID 该存储是聚焦委托更新索引的存储 还有存储的永久性历史纪录令牌 该存储是聚焦委托更新索引的存储 你可以用这两个键判断 你有兴趣的存储是否已经索引到 最新的永久性历史纪录令牌 如果未启用索引 你可以将自己观察者的身份 从通知移除 今年之前 只有一种方式能删除 你的应用程序索引的数据 那就是实现 移除索引条目的Core Spotlight API 或是将Core Data中 整个客户端图形删除 关键地 iOS 15和macOS Monterey 新推出的功能中 Core Data给了开发者 不需删除客户端图形 就能管理聚焦索引的新方式 这对于用户隐私来说是大好消息 首先 代码会停止索引 然后 调用deleteSpotlightIndex 最后 在完成处理程序中 处理任何出现的错误 注意调用这个方法可能会返回 来自较低层的依赖项的错误 例如Core Data和Core Spotlight 你应该准备好处理它们 现在我已经示范了如何自定义 聚焦委托的实现 我们来验证我们的设置 方式是用Core Spotlight API 添加全文检索到标记应用程序 结果会是之前索引的东西 先定义一个 PhotosViewController的扩展 该扩展采用 UISearchResultsUpdating协议 和一个函数 updateSearchResults (for Controller) 标记用户界面 有一个UISearchController 我们会从那个搜索控制器的搜索栏 获得用户输入 如果用户输入为空白 从数据提供程序提取所有图像 然后重新加载集合视图 因为没有搜索查询 现在我们来处理有搜索查询的案件 一开始先通过转义 清理用户输入字符串 接着 用已清理的用户输入字符串 定义查询字符串 查询字符串根据 和CSSearchableItemAttributeSet 对象中的属性 有关联的值操作 这个例子中 代码会根据 前一步骤设置的Keywords特性操作 搜索查询中使用 修改器c、d和w c代表不分大小写 d代表不分音符号 而w代表基于文字的搜索 现在创建一个CSSearchQuery对象 方式是指定 刚创建的带格式查询字符串 和与CSSearchableItemAttributeSet 定义的属性相符合的 特性名称数组 此搜索查询对象 管理搜索应用程序内容时 应该用的条件 应用程序内容是你先前 用聚焦委托API索引的内容 在那之后 设置foundItemsHandler 这个处理程序会被重复调用于 匹配先前定义的搜索查询的项目 查询的completionHandler 会被调用一次 在这检查是否有错误 可能需要执行错误处理 若没有错误 调度一块到主队列上 以用我们的数据提供程序 对聚焦找到的项目执行提取 并在用户界面中加载 最后最重要的部分 别忘了启动查询 现在标记应用程序有个聚焦委托 索引它的内容 那些数据从应用程序中解放了! 我进入聚焦并搜索 我之前添加的标记时 它返回两个结果 标记名称本身 还有我用关键词 “天然桥国家公园” 标记的那张照片 最后总结 我们了解了 NSCoreDataCoreSpotlightDelegate 还有它可以如何帮助你的用户 用聚焦搜索找到 在你的应用程序里面和外面的内容 快速简单地设置聚焦委托 不需写一堆代码就能开始索引 还有用这次推出的一些全新可用API 自定义我们的聚焦委托 希望你觉得这些信息很有用 会考虑在你的项目中采用 NSCoreDataCoreSpotlightDelegate 帮助用户找到他们的内容 祝你有美好的WWDC! ♪
-
-
2:40 - Creating a NSCoreDataCoreSpotlightDelegate
let spotlightDelegate = NSCoreDataCoreSpotlightDelegate(forStoreWith: description, coordinator: coordinator) spotlightDelegate.startSpotlightIndexing()
-
5:24 - Adding a NSCoreDataCoreSpotlightDelegate to a CoreDataStack
import Foundation import CoreData class CoreDataStack { private (set) var spotlightIndexer: TagsSpotlightDelegate? lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "Tags") guard let description = container.persistentStoreDescriptions.first else { fatalError("###\(#function): Failed to retrieve a persistent store description.") } description.type = NSSQLiteStoreType description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) container.loadPersistentStores(completionHandler: { (_, error) in guard let error = error as NSError? else { return } fatalError("###\(#function): Failed to load persistent stores:\(error)") }) spotlightIndexer = TagsSpotlightDelegate(forStoreWith: description, coordinator: container.persistentStoreCoordinator) container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy container.viewContext.automaticallyMergesChangesFromParent = true do { try container.viewContext.setQueryGenerationFrom(.current) } catch { fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)") } return container }() }
-
6:24 - Creating TagsSpotlightDelegate
class TagsSpotlightDelegate: NSCoreDataCoreSpotlightDelegate { override func domainIdentifier() -> String { return "com.example.apple-samplecode.tags" } override func indexName() -> String? { return "tags-index" } override func attributeSet(for object: NSManagedObject) -> CSSearchableItemAttributeSet? { if let photo = object as? Photo { let attributeSet = CSSearchableItemAttributeSet(contentType: .image) attributeSet.identifier = photo.uniqueName attributeSet.displayName = photo.userSpecifiedName attributeSet.thumbnailData = photo.thumbnail?.data for case let tag as Tag in photo.tags ?? [] { if let name = tag.name { if attributeSet.keywords != nil { attributeSet.keywords?.append(name) } else { attributeSet.keywords = [name] } } } return attributeSet } else if let object as? Tag { let attributeSet = CSSearchableItemAttributeSet(contentType: .text) attributeSet.displayName = tag.name return attributeSet } return nil } }
-
9:51 - Customizing PhotosViewController with Spotlight delegate functionality
class PhotosViewController: UICollectionViewController { @IBOutlet var generateDefaultPhotosItem: UIBarButtonItem! @IBOutlet var deleteSpotlightIndexItem: UIBarButtonItem! @IBOutlet var startStopIndexingItem: UIBarButtonItem! private var isTagging = false private var spotlightFoundItems = [CSSearchableItem]() private static let defaultSectionNumber = 0 private var searchQuery: CSSearchQuery? var spotlightUpdateObserver: NSObjectProtocol? private lazy var spotlightIndexer: TagsSpotlightDelegate = { let appDelegate = UIApplication.shared.delegate as? AppDelegate return appDelegate!.coreDataStack.spotlightIndexer! }() override func viewDidLoad() { super.viewDidLoad() // ... toggleSpotlightIndexing(enabled: true) } @IBAction func deleteSpotlightIndex(_ sender: Any) { toggleSpotlightIndexing(enabled: false) spotlightIndexer.deleteSpotlightIndex(completionHandler: { (error) in if let err = error { print("Encountered error while deleting Spotlight index data, \(err.localizedDescription)") } else { print("Finished deleting Spotlight index data.") } }) } @IBAction func toggleSpotlightIndexingEnabled(_ sender: Any) { if spotlightIndexer.isIndexingEnabled == true { toggleSpotlightIndexing(enabled: false) } else { toggleSpotlightIndexing(enabled: true) } } private func toggleSpotlightIndexing(enabled: Bool) { if enabled { spotlightIndexer.startSpotlightIndexing() startStopIndexingItem.image = UIImage(systemName: "pause") } else { spotlightIndexer.stopSpotlightIndexing() startStopIndexingItem.image = UIImage(systemName: "play") } let center = NotificationCenter.default if spotlightIndexer.isIndexingEnabled && spotlightUpdateObserver == nil { let queue = OperationQueue.main spotlightUpdateObserver = center.addObserver(forName: NSCoreDataCoreSpotlightDelegate.indexDidUpdateNotification, object: nil, queue: queue) { (notification) in let userInfo = notification.userInfo let storeID = userInfo?[NSStoreUUIDKey] as? String let token = userInfo?[NSPersistentHistoryTokenKey] as? NSPersistentHistoryToken if let storeID = storeID, let token = token { print("Store with identifier \(storeID) has completed ", "indexing and has processed history token up through \(String(describing: token)).") } } } else { if spotlightUpdateObserver == nil { return } center.removeObserver(spotlightUpdateObserver as Any) } } }
-
13:13 - Adding full-text search to PhotosViewController
extension PhotosViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { guard let userInput = searchController.searchBar.text, !userInput.isEmpty else { dataProvider.performFetch(predicate: nil) reloadCollectionView() return } let escapedString = userInput.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") let queryString = "(keywords == \"" + escapedString + "*\"cwdt)" searchQuery = CSSearchQuery(queryString: queryString, attributes: ["displayName", "keywords"]) // Set a handler for results. This will be a called 0 or more times. searchQuery?.foundItemsHandler = { items in DispatchQueue.main.async { self.spotlightFoundItems += items } } // Set a completion handler. This will be called once. searchQuery?.completionHandler = { error in guard error == nil else { print("CSSearchQuery completed with error: \(error!).") return } DispatchQueue.main.async { self.dataProvider.performFetch(searchableItems: self.spotlightFoundItems) self.reloadCollectionView() self.spotlightFoundItems.removeAll() } } // Start the query. searchQuery?.start() } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。