16、Mac应用开发:集成Spotlight与Quick Look

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,为用户提供更便捷的搜索和预览功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值