Mac Quicklook开发

介绍

经常使用Mac电脑的同学应该熟悉 quicklook,在 Finder中,选中一个文件,点击空格键就会弹出一个窗口,显示文件内容(在文件可以预览的情况下),如下图所示:

 Mac Finder Quicklook window

这个是Mac 提供的系统功能,自带支持office相关格式的文件,纯文本文件等格式,具体文件格式如下:

  • iWork and Microsoft Office documents
  • Images
  • Live Photos
  • Text files
  • PDFs
  • Audio and video files
  • Augmented reality objects that use the USDZ file format (iOS and iPadOS only)

如果是想预览系统不支持的文件格式,有什么办法么?Mac也给开发者提供了Framework用于开发特定格式文件的预览视图。有很多开发者提供了免费的 quicklook插件,可以在这个网站上查找自己需要的:https://www.quicklookplugins.com/

quicklook Framework

分为两个部分,一个是提供方:支持特定格式文件的预览,另一个是使用方:调用 quicklook预览文件。
在 IOS App的开发中,我们一般是使用方,如在 app内显示、预览文件,背后的技术就是quicklook;
在 Mac App的开发中,我们还可以是提供方,如自己开发的app提供了一种独有的文档格式,也想让用户在不打开app时,方便预览,就可以开发quicklook plugin以支持自有文档格式的预览;
本文主要聚焦于在开发Mac App时,如何在app内预览文件。

简单接入

根据官方文档可知,每个应用程序都有一个共享的单实例QLPreviewPanel,可用于显示预览内容
如果需求简单,只是显示一个预览窗口,可直接调用接口,传入要预览的文件路径,会自动显示预览窗口

1
2
3
4
5
6
7
// QLItem 是 QLPreviewItem的实现
let qlItem = QLItem(previewItemURL: .init(URL(fileURLWithPath: " pathoffile.png")))
// QLData 是QLPreviewPanelDataSource的实现,最佳实践是使用delegate,这里仅是demo
let qlData: QLData = .init(item: qlItem)
QLPreviewPanel.shared().dataSource = qlData;
// 显示到前台
QLPreviewPanel.shared().makeKeyAndOrderFront(nil)

效果如下图
mac quicklook demo

定制化

上面我们成功显示了预览窗口与文件内容,可能满足简单的需求,如果要更进一步,需要解决两个问题:

  1. 窗口定制:如上图所示,预览窗口是固定的样式,如果想定制化窗口,就不能使用默认的窗口,需要把内容渲染到自定义的窗口上
  2. 兼容处理:如果是不能预览的文件,默认的提示也不明显,这时我们需要提前知道哪些文件格式能预览,对不能预览的文件提供特别处理

我们先来解决问题2,我们先来确定预览模块是如何确定哪些文件能否预览,需要调用哪个预览程序来渲染内容。经简单测试:

  1. 无后缀名的文件无法预览
  2. 修改后缀名后文件无法预览

可知,预览系统是通过文件后缀名来确定文件是否能预览,和需要调用的预览程序的。通过官方文档可知:

  1. 通过/usr/bin/qlmanage -m plugins可列出支持预览的文件格式与对应的预览程序
  2. 特殊情况下,如果上述命令执行失败,我们还可以通过手动查找的方式确定,预览程序默认存放位置有三处:
    • /System/Library/QuickLook/
    • /Library/QuickLook/
    • ~/Library/QuickLook/

遍历上述目录,然后通过每个plugin中的/Contents/Info.plist文件内容来确定哪些文件格式可被预览

1
2
3
4
5
6
7
8
9
10
11
12
// 以 /System/Library/QuickLook/Icon.qlgenerator/Contents/Info.plist 文件为例
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>QLGenerator</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.iconset</string> // 代表此plugin可预览的文件格式
</array>
</dict>
</array>

通过上述两种方式可收集到所有可预览的文件格式。下面来解决问题1窗口定制,通过查找官方文档,发现QLPreviewView在自定义的窗口中放置QLPreviewView,然后设置previewItem为文件本地路径即可。

one more thing

这样实现后与Finder中的quicklook对比,发现有些文件格式没在可预览列表中,但可正常预览,还有一个隐藏知识点:文件格式是有继承关系的,如果格式无法预览,默认的quicklook会向上查找父级的格式是否可预览,如果可预览会调用父级的预览程序来提供预览。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func canQuickLook(filePath: String) -> Bool {
// 获取文件扩展名
let fileExt = (filePath as NSString).pathExtension
// 通过扩展名获取对应的文档格式
var type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExt as CFString, nil)?.takeRetainedValue()
print(type)
if (type != nil) {
var uti = type! as NSString
var app_url = UTI_APP_DICT[uti as String]
if (app_url != nil) {
return true
}
// 获取父级及以上的文档格式
let dict = UTTypeCopyDeclaration(type!)
print(dict)
if (dict != nil) {
let map = dict?.takeRetainedValue() as! [CFString: AnyObject]
let tree = map["UTTypeConformsTo" as CFString] as? [CFString]
if (tree != nil) {
for item in tree! {
uti = item as NSString
// 只要有一个父级格式在可预览字典中即可预览
app_url = UTI_APP_DICT[uti as String]
if (app_url != nil) {
return true
}
}
}
}
}
return false
}