Mac应用开发:集成Spotlight与Quick Look
1. 搜索功能与开发背景
在开发桌面应用时,我们可以添加一个
NSSearchField
,它能够搜索食谱名称、食材、描述或其他我们需要的信息。绑定完成后,一个基本的搜索字段就添加好了。运行应用程序后,在搜索字段中输入的文本会影响食谱列表。
开发Mac OS X应用需要兼顾功能与质量。Spotlight和Quick Look集成虽然不是用户在尝试新应用时会主动寻找的功能,但当他们偶然发现时会感到惊喜。然而,很多开发者并未处理好这方面的集成。这可能是因为Spotlight与Core Data不太兼容,或者该功能过于抽象。但可以确定的是,集成Spotlight是未来的正确方向,因为Spotlight会长期存在,并且用户会更频繁、更有创意地使用它。
2. Spotlight与Core Data的冲突
Spotlight基于单个文件的元数据工作,而Core Data将所有数据存储在一个文件中。由于Spotlight是通过文件的元数据来发现文件信息的,所以它在处理像Core Data这样的单文件设计时效果不佳。例如,Tiger首次发布时,一些应用(如Entourage)由于单文件设计,与Spotlight的兼容性不好,苹果甚至为此重新架构了Mail应用。
3. 集成Spotlight的方案
3.1 多文件问题的解决
对于食谱应用,理想情况下,我们希望Core Data仓库中的每个食谱都有一个Spotlight “记录”。为了让Spotlight正常工作,每个食谱需要在磁盘上有一个对应的文件及其关联的元数据。因此,我们会创建这些额外的文件,不过由于所有数据都存储在Core Data仓库中,这些文件无需存储实际数据,仅用于Spotlight(和Quick Look)使用。
3.2 元数据文件的创建
为了使导入器尽可能快,我们会将元数据存储在为Spotlight创建的文件中,而不是在Spotlight请求时从Core Data仓库中查找元数据。这样,导入器只需处理元数据文件,无需初始化整个Core Data “栈”(即
NSManagedObjectContext
、
NSPersistentStoreCoordinator
和
NSManagedObjectModel
)。
3.2.1 创建NSManagedObject子类
我们创建一个
NSManagedObject
子类
PPRRecipeMO
来处理生成包含所有元数据的
NSDictionary
。以下是相关代码:
// Spotlight/PPRecipes/PPRRecipeMO.swift
class PPRRecipeMO: NSManagedObject {
@NSManaged var desc: String?
@NSManaged var imagePath: String?
@NSManaged var lastUsed: NSDate?
@NSManaged var name: String?
@NSManaged var serves: NSNumber?
@NSManaged var type: String
@NSManaged var author: NSManagedObject?
@NSManaged var ingredients: [NSManagedObject]?
}
同时,我们需要确保在最新的数据模型中更改
Class
设置,让Core Data使用我们的子类而非默认的
NSManagedObject
。
3.2.2 实现元数据方法
元数据文件应包含足够的信息来填充Spotlight和Quick Look,但又不能太大太繁琐。对于元数据文件,我们真正需要的信息如下:
- 食谱名称
- 可供人数
- 食谱图片
- 最后使用时间
- 制备说明
由于图片可能太大,我们将图片的路径放入元数据文件,而不是实际的图片。此外,我们还需要添加一个非用户可见的项,用于链接回Core Data仓库中的食谱记录。以下是实现元数据方法的代码:
// Spotlight/PPRecipes/PPRRecipeMO.swift
func metadata() -> NSDictionary {
let metadata = NSMutableDictionary()
guard let name = name else { fatalError("Malformed Recipe, no name") }
metadata[kPPItemTitle] = name
if let desc = desc { metadata[kPPItemTextContent] = desc }
if let author = author, let name = author.valueForKey("name") {
metadata[kPPItemAuthors] = name
}
metadata[kPPImagePath] = imagePath
metadata[kPPItemLastUsedDate] = lastUsed
metadata[kPPServes] = serves
metadata[kPPObjectID] = objectID.URIRepresentation().absoluteString
return metadata
}
3.2.3 实现元数据文件名方法
为了让用户能在Finder中查看实际的元数据文件,文件名应代表食谱,而不是抽象的名称。我们使用食谱的名称属性作为文件名,代码如下:
// Spotlight/PPRecipes/PPRRecipeMO.swift
func metadataFilename() -> String {
guard let name = name else { fatalError("Malformed Recipe, no name") }
return "\(name).grokkingrecipe"
}
3.3 生成和更新元数据文件
我们需要添加功能来填充这些元数据文件并保持其最新状态。理想情况下,每次保存
NSManagedObjectContext
时,我们都要刷新元数据文件。以下是更新后的
saveContext()
方法:
// Spotlight/PPRecipes/PPRDataController.swift
func saveContext() {
guard let main = mainContext else {
fatalError("save called before mainContext is initialized")
}
main.performBlockAndWait({
if !main.hasChanges {
return
}
let recipeFilter = { (mo:NSManagedObject) -> Bool in
if mo.isKindOfClass(PPRRecipeMO.self) {
return true
} else {
return false
}
}
let deleted = main.deletedObjects.filter(recipeFilter).map {
(mo) -> String in
return mo.valueForKey("metadataFilename") as! String
}
var existing = [NSManagedObject]()
existing.appendContentsOf(main.insertedObjects.filter(recipeFilter))
existing.appendContentsOf(main.updatedObjects.filter(recipeFilter))
do {
try main.save()
self.updateMetadataForObjects(existing, andDeletedObjects:deleted)
} catch {
fatalError("Failed to save mainContext: \(error)")
}
})
}
在这个更新后的
save()
方法中,我们在调用写入器
NSManagedObjectContext
的
save()
方法之前,会获取已删除、更新或插入的对象信息。保存完成后,这些信息将不再可用。我们还会过滤出仅与食谱实体相关的信息,处理已删除对象时提取所需信息,将更新和插入的对象合并处理。保存成功后,我们会更新元数据。
更新元数据的具体步骤如下:
1. 检查是否有需要更新或删除的内容。
2. 确认缓存目录和元数据目录是否存在,若不存在则创建。
3. 删除不再适用的文件。
4. 处理现有或新的食谱,将元数据写入磁盘,并更新文件属性以隐藏文件扩展名。
以下是相关代码:
// Spotlight/PPRecipes/PPRDataController.swift
if existing.count == 0 && deleted.count == 0 { return }
let fileManager = NSFileManager.defaultManager()
do {
try fileManager.createDirectoryAtURL(metadataFolderURL,
withIntermediateDirectories: true, attributes: nil)
} catch let error as NSError {
if error.code != 518 { //Expected error
fatalError("Unexpected error creating metadata: \(error)")
}
}
for path in deleted {
let fileURL = metadataFolderURL.URLByAppendingPathComponent(path)
do {
try fileManager.removeItemAtURL(fileURL)
} catch {
print("Error deleting: \(error)")
}
}
let attributes = [NSFileExtensionHidden:true]
for object in existing {
guard let recipe = object as? PPRRecipeMO else {
fatalError("Non-recipe unexpected")
}
let metadata = recipe.metadata()
let filename = recipe.metadataFilename()
let fileURL = metadataFolderURL.URLByAppendingPathComponent(filename)
guard let path = fileURL.path else {
fatalError("Failed to resolve path")
}
metadata.writeToFile(path, atomically:true)
do {
try fileManager.setAttributes(attributes, ofItemAtPath: path)
} catch {
fatalError("Failed to update attributes: \(error)")
}
}
如果是现有用户,在添加Spotlight集成后,我们可以使用后台队列来更新元数据,代码如下:
// Spotlight/PPRecipes/PPRDataController.swift
dispatch_async(queue) {
self.verifyAndUpdateMetadata()
}
private func verifyAndUpdateMetadata() {
guard let path = metadataFolderURL.path else {
fatalError("Failed to resolve metadata folder")
}
if NSFileManager.defaultManager().fileExistsAtPath(path) { return }
let t = NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType
let child = NSManagedObjectContext(concurrencyType: t)
child.performBlock {
let fetch = NSFetchRequest(entityName: "Recipe")
do {
let r = try child.executeFetchRequest(fetch) as! [NSManagedObject]
self.updateMetadataForObjects(r, andDeletedObjects: [])
} catch {
fatalError("Failed to retrieve recipes: \(error)")
}
}
}
3.4 创建Spotlight导入器
3.4.1 统一类型标识符(UTIs)
Spotlight和Quick Look使用UTIs而非文件名扩展名来将磁盘上的文件与导入器和生成器关联起来。UTI是一个唯一的字符串,用于标识给定文件中存储的数据类型。我们的应用使用
com.pragprog.grokkingrecipes
作为唯一的捆绑标识符,同样使用该UTI作为
LSItemContentTypes
的值来标识文件。以下是相关的
Info.plist
配置:
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Grokking Recipes</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>grokkingrecipe</string>
</array>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.content</string>
</array>
<key>UTTypeDescription</key>
<string>Grokking Recipe</string>
<key>UTTypeIdentifier</key>
<string>com.pragprog.grokkingrecipe</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<string>grokkingrecipe</string>
</dict>
</dict>
</array>
这个配置描述了我们的UTI,并告诉Mac OS X如何将其与不同的文件扩展名关联起来,同时向操作系统描述了数据类型的名称和在UTI树中的位置。
3.4.2 Xcode子项目
Spotlight导入器是一个独立的应用,我们将其设置为主要项目的依赖或子项目。具体步骤如下:
1. 在Xcode中创建一个Spotlight导入器项目,将其保存到主食谱项目的目录中,命名为
SpotlightPlugin
。
2. 将子项目拖入主项目。
3. 打开主项目的目标,选择“General”选项卡。
4. 添加子项目作为依赖。
5. 为主项目的目标添加一个新的复制阶段,将其目标设置为“Plugins”,路径设置为
Contents/Library/Spotlight
。
6. 将Spotlight插件拖入新的构建阶段。
这样,每次清理或构建主项目时,子项目也会相应地清理或构建,并且子项目会使用与主项目相同的设置进行构建。
3.4.3 将Spotlight导入器与UTI关联
我们需要更新Spotlight子项目的
Info.plist
,让操作系统知道该导入器处理的UTI。以下是相关配置:
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>MDImporter</string>
<key>LSItemContentTypes</key>
<array>
<string>com.pragprog.grokkingrecipe</string>
</array>
</dict>
</array>
通过这个配置,Mac OS X知道使用该导入器来检索我们元数据文件的信息。
3.4.4 构建Spotlight导入器
导入器的实际代码非常简单。我们将元数据文件加载回
NSDictionary
,遍历其键并将关联的值添加到传入的
CFMutableDictionaryRef
中。以下是相关代码:
// Spotlight/SpotlightPlugin/GetMetadataForFile.m
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#import <Foundation/Foundation.h>
Boolean GetMetadataForFile(void* thisInterface,
CFMutableDictionaryRef attributes,
CFStringRef contentTypeUTI,
CFStringRef pathToFile)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSDictionary *meta;
meta = [NSDictionary dictionaryWithContentsOfFile:(NSString*)pathToFile];
for (NSString *key in [meta allKeys]) {
[(id)attributes setObject:[meta objectForKey:key] forKey:key];
}
[pool release], pool = nil;
return TRUE;
}
由于我们在C函数中运行,需要使用
NSAutoreleasePool
来避免内存泄漏。
3.5 测试Spotlight导入器
我们可以通过以下两种方式测试导入器:
1. 运行应用程序生成元数据文件,然后使用命令行工具
mdimport
测试导入器。首先,告诉Spotlight加载我们的导入器:
mdimport -r ${path to our project}/build/Debug/GrokkingRecipes.app/Contents/Library/Spotlight/SpotlightPlugin.mdimporter
然后,查询导入器:
cd ~/Library/Caches/Metadata/GrokkingRecipes
mdimport -d2 Test.grokkingrecipe
我们可以更改调试级别(从1到4)以显示不同数量的元数据文件信息。使用级别2可以确认导入器是否正常工作,并获取文件中数据的摘要。
2. 直接在右上角点击Spotlight放大镜,输入一个食谱的名称进行搜索。
3.6 接受元数据文件
由于我们将元数据文件链接到了主应用程序,当用户尝试打开元数据文件时,Mac OS X会尝试打开我们的应用程序并将文件传递给我们。我们需要让应用程序接受打开文件的请求,以下是相关代码:
// Spotlight/PPRecipes/AppDelegate.swift
func application(sender: NSApplication, openFile filename: String) -> Bool {
guard let metadata = NSDictionary(contentsOfFile: filename) else {
print("Unable to build dictionary from file")
return false
}
guard let objectIDString = metadata[kPPObjectID] as? String else {
print("ObjectID was not a string")
return false
}
guard let objectURI = NSURL(string: objectIDString) else {
print("ObjectID could not be formed into a URL")
return false
}
guard let moc = dataController.mainContext else {
print("Main context is nil")
return false
}
guard let psc = moc.persistentStoreCoordinator else {
print("PSC is nil")
return false
}
// 后续处理代码可根据需求添加
return true
}
通过以上步骤,我们可以成功地将Spotlight集成到我们的食谱应用中,让用户能够更方便地搜索和访问食谱信息。
以下是创建和更新元数据文件的流程图:
graph TD;
A[开始] --> B[检查是否有更改];
B -- 无更改 --> C[结束];
B -- 有更改 --> D[过滤食谱实体];
D --> E[处理已删除对象];
D --> F[合并更新和插入对象];
E --> G[保存上下文];
F --> G;
G -- 保存失败 --> H[终止应用];
G -- 保存成功 --> I[确认目录];
I --> J[删除不再适用的文件];
J --> K[处理现有或新食谱];
K --> L[更新文件属性];
L --> M[结束];
以下是测试Spotlight导入器的步骤表格:
| 步骤 | 操作 | 命令示例 |
| ---- | ---- | ---- |
| 1 | 生成元数据文件 | 运行应用程序 |
| 2 | 加载导入器 |
mdimport -r ${path to our project}/build/Debug/GrokkingRecipes.app/Contents/Library/Spotlight/SpotlightPlugin.mdimporter
|
| 3 | 查询导入器 |
cd ~/Library/Caches/Metadata/GrokkingRecipes
mdimport -d2 Test.grokkingrecipe
|
| 4 | 搜索食谱 | 点击Spotlight放大镜,输入食谱名称 |
4. Quick Look集成概述
虽然Quick Look在我们的示例应用中并非完全适用(因为我们只有一个数据文件且该文件隐藏在
Library/Application Support
目录中),但了解它如何用于基于文档的Core Data应用非常有用,因为这能使应用在Finder、Spotlight、Time Machine、Mail等许多应用中更易于查找。而且,Quick Look和Spotlight能很好地集成。当用户对Spotlight搜索结果使用Quick Look时,我们希望他们看到的是食谱信息,而不是通用文件的图片。
4.1 Quick Look与Spotlight的相似性
在Mac OS X和Finder中,Quick Look和Spotlight的处理方式非常相似。它们都依赖于UTIs来关联文件和相应的处理程序(Spotlight的导入器和Quick Look的生成器)。
4.2 集成Quick Look的优势
对于基于文档的应用,集成Quick Look可以提供以下优势:
-
提高可发现性
:用户可以在Finder、Spotlight等工具中快速预览文件内容,无需打开应用。
-
增强用户体验
:提供更直观的文件查看方式,使用户能够快速判断文件是否是他们需要的。
-
与其他应用集成
:方便在Mail、Time Machine等应用中查看文件。
5. 数据存储方式的选择
5.1 单文件与多文件的权衡
在整个开发过程中,我们的应用使用了单个Core Data文件,这是为了清晰和专注于Core Data的使用。但根据应用的设计,可能会有不同的选择:
-
文档型应用
:通常适合为每个文档使用一个Core Data存储库。在这种情况下,Spotlight和Quick Look的集成会更容易,因为每个文件都有自己的元数据。
-
非文档型应用
:更倾向于使用单个Core Data存储库。虽然单个文件与Spotlight的兼容性较差,但它能更方便地管理数据,确保数据的逻辑一致性和可重复性。
5.2 选择建议
在选择数据存储方式时,需要考虑以下因素:
| 因素 | 单文件存储 | 多文件存储 |
| ---- | ---- | ---- |
| 数据管理 | 方便数据的集中管理和维护 | 每个文件独立,管理相对复杂 |
| Spotlight集成 | 困难,需要额外的处理 | 容易,每个文件有独立的元数据 |
| 应用类型 | 适合非文档型应用 | 适合文档型应用 |
6. 总结与展望
6.1 集成成果总结
通过上述步骤,我们成功地将Spotlight集成到了食谱应用中。用户现在可以在Spotlight中搜索食谱,并直接在应用中打开搜索结果。同时,我们也了解了Quick Look的集成原理和优势,为后续的开发提供了思路。
6.2 未来改进方向
- 性能优化 :虽然我们已经采取了一些措施来提高导入器的性能,但仍可以进一步优化,例如使用更高效的数据结构和算法。
- Quick Look集成 :在后续的开发中,可以进一步完善Quick Look的集成,为用户提供更丰富的预览体验。
- 用户反馈 :收集用户的反馈,根据用户的需求和使用习惯,对应用进行改进和优化。
以下是集成Spotlight和Quick Look的整体流程图:
graph LR;
A[开始开发] --> B[添加搜索功能];
B --> C[处理Spotlight与Core Data冲突];
C --> D[创建元数据文件];
D --> E[生成和更新元数据];
E --> F[创建Spotlight导入器];
F --> G[测试导入器];
G --> H[接受元数据文件];
C --> I[考虑Quick Look集成];
I --> J[权衡数据存储方式];
J --> K[选择合适的存储方式];
K --> L[持续优化和改进];
以下是开发过程中的关键步骤列表:
1. 添加
NSSearchField
实现基本搜索功能。
2. 解决Spotlight与Core Data的兼容性问题。
3. 创建和管理元数据文件。
4. 开发Spotlight导入器并进行测试。
5. 让应用接受元数据文件的打开请求。
6. 考虑Quick Look集成和数据存储方式的选择。
7. 持续优化应用性能和用户体验。
通过以上步骤和方法,我们可以开发出一个功能强大、用户体验良好的Mac应用,集成Spotlight和Quick Look,为用户提供更便捷的搜索和预览功能。
超级会员免费看
13

被折叠的 条评论
为什么被折叠?



