版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.05.16 星期四 |
前言
今天翻阅苹果的API文档,发现多了一个框架就是FileProvider,看了下才看见是iOS11.0新添加的框架,这里我们就一起来看一下框架FileProvider。感兴趣的看下面几篇文章。
1. FileProvider框架详细解析 (一) —— 基本概览(一)
开始
首先看下写作环境
Swift 5, iOS 12, Xcode 10
在本教程中,您将了解File Provider
框架以及如何实现自己的File Provider extension
以公开应用程序自己的内容。
iOS 11中首次引入了File Provider extension
,可通过iOS Files
应用程序访问您的应用程序管理的内容。 此外,其他应用程序可以使用UIDocumentBrowserViewController或UIDocumentPickerViewController类来访问您的应用程序的数据。
File Provider extension
的主要任务是:
- 创建表示远程内容的文件占位符。
- 拦截从主机应用程序读取,以便文档可以下载或更新。
- 在更新文档后发出通知,以便更改可以上载到远程服务器。
- 枚举存储的文档和目录。
- 对文档执行操作,例如重命名,移动或删除。
您将使用Heroku Button配置托管文件的服务器。 服务器设置完成后,您将配置扩展以枚举服务器的内容。
打开Favart-Starter
文件夹并在Xcode中打开Favart.xcodeproj
。 确保您已选择Favart scheme
,然后构建并运行该应用程序,您将看到以下内容:
该应用程序提供了一个基本视图,用于教育用户如何启用File Provider extension
,因为您实际上不会在应用程序本身内执行任何操作。每次在本教程中构建和运行应用程序时,您都将返回主屏幕并打开Files
应用程序以访问您的扩展程序。
注意:如果要在实际设备上运行示例项目,除了为两个目标设置开发团队之外,还必须在
Configuration
文件夹中编辑Favart.xcconfig
。将bundle identifier
更新为唯一值。示例项目将此值用于两个目标中的
PRODUCT_BUNDLE_IDENTIFIER
构建设置,以及Provider.entitlements
中的App Group
标识符和Info.plist
中的关联NSExtensionFileProviderDocumentGroup
值。如果不在项目中一致地更新值,则可能会看到模糊且难以调试的错误。使用自定义构建设置是保持平稳运行的好方法。
示例项目包括您将用于File Provider extension
的基本组件:
- NetworkClient.swift包含用于与
Heroku
服务器通信的网络客户端。 - FileProviderExtension.swift本身具有
File Provider extension
。 - FileProviderEnumerator.swift包含枚举器,用于列出目录的内容。
- Models是一个包含完成扩展所需模型的组。
Setting Up the Back End With Heroku
首先,您需要自己的后端服务器实例。 幸运的是,使用Heroku Button
很容易。 单击下面的按钮以访问Heroku dashboard
。
在您注册免费帐户或登录Heroku
后,您将最终进入类似以下内容的页面。
在此页面上,您可以选择为应用程序命名。 您可以选择一个名称,如果您将该字段留空,Heroku
将为您生成一个名称。 由于无需担心其他配置,请单击Deploy app
按钮,片刻之后,您的后端将启动并运行。
Heroku
完成部署应用程序后,单击底部的View
按钮。这将带您到承载后端实例的URL。在应用程序的根目录下,您应该看到一条JSON消息,是Hello world!
最后,您需要复制Heroku
实例的URL。您只需要域部分,它应如下所示:{app-name} .herokuapp.com
。
在starter
项目中,打开Provider / NetworkClient.swift
。在文件的顶部,您应该看到一条警告:Add your Heroku URL here
。删除警告并使用您的URL
替换components.host
占位符字符串。
这样就完成了服务器配置。接下来,您将定义File Provider
所依赖的模型。
Defining an NSFileProviderItem
首先,File Provider
需要一个符合NSFileProviderItem的模型。此模型将提供有关文件提供程序管理的文件的信息。初始化项目包含FileProviderItem.swift
中的FileProviderItem
,您将使用它,但在符合协议之前需要做一些工作。
虽然该协议有27个属性,但只需要4个。可选属性为File Provider
框架提供有关每个文件的更多详细信息,并启用其他功能。在本教程中,您将专注于所需的属性:itemIdentifier
,parentItemIdentifier
,filename
和typeIdentifier
。
itemIdentifier
为模型提供唯一可识别的密钥。File Provider
使用parentIdentifier
来跟踪其在扩展的层次结构中的位置。
filename
是Files/em> app<. finally>typeIdentifier
是项目的uniform type identifier或UTI
中展示的item名称。
在FileProviderItem
符合NSFileProviderItem
之前,它需要一种方法来处理来自后端的数据。 MediaItem
定义了一个从后端返回的简单模型。 MediaItemReference
不是直接在FileProviderItem
中使用这个模型,而是处理扩展的一些额外逻辑以弥补差距。
您将在本教程中使用MediaItemReference
有两个原因:
- 1)
Heroku
上的后端非常简单。它无法提供NSFileProviderItem
所需的所有信息,因此您需要在其他地方获取它。 - 2)
File Provider extension
也很简单。更完整的File Provider extension
需要使用诸如Core Data
之类的东西在本地持久保存后端返回的信息,以便在扩展的生命周期中稍后引用它。
为了将教程的重点放在文件File Provider extension
本身上,您将使用MediaItemReference
通过将四个必需属性的数据嵌入到URL
对象中来作弊。然后,您将对该URL进行base64
编码,并将其编码为NSFileProviderItemIdentifier
。您不需要自己保留任何内容,因为NSFileProviderExtension
会为您处理它。
要开始构建模型,请打开Provider / MediaItemReference.swift
并在MediaItemReference
中添加以下内容:
// 1
private let urlRepresentation: URL
// 2
private var isRoot: Bool {
return urlRepresentation.path == "/"
}
// 3
private init(urlRepresentation: URL) {
self.urlRepresentation = urlRepresentation
}
// 4
init(path: String, filename: String) {
let isDirectory = filename.components(separatedBy: ".").count == 1
let pathComponents = path.components(separatedBy: "/").filter {
!$0.isEmpty
} + [filename]
var absolutePath = "/" + pathComponents.joined(separator: "/")
if isDirectory {
absolutePath.append("/")
}
absolutePath = absolutePath.addingPercentEncoding(
withAllowedCharacters: .urlPathAllowed
) ?? absolutePath
self.init(urlRepresentation: URL(string: "itemReference://\(absolutePath)")!)
}
这是你做的:
- 1) 对于本教程,
URL
将表示NSFileProviderItem
所需的大部分信息。 - 2) 此计算属性确定当前项是否是文件系统的根。
- 3) 您将此初始化程序设为私有以防止从模型外部使用。
- 4) 从后端读取数据时,您将使用此初始化程序。 您假设如果文件名不包含句点,则它必须是目录,因为初始化程序无法推断其类型。
在添加最终初始化程序之前,请使用以下内容替换文件顶部的import语句:
import FileProvider
接下来,在前面的代码下面添加以下初始化器:
init?(itemIdentifier: NSFileProviderItemIdentifier) {
guard itemIdentifier != .rootContainer else {
self.init(urlRepresentation: URL(string: "itemReference:///")!)
return
}
guard
let data = Data(base64Encoded: itemIdentifier.rawValue),
let url = URL(dataRepresentation: data, relativeTo: nil)
else {
return nil
}
self.init(urlRepresentation: url)
}
大部分扩展都将使用此初始化程序。 记下scheme
为itemReference://
。 您可以单独处理根容器标识符,以确保正确设置其URL
路径。
对于其他项,通过将标识符的原始值转换为base64
编码的数据来检索URL表示。 URL中的信息来自首先枚举实例的网络请求。
现在初始化器已经不在了,是时候为这个模型添加一些属性了。 首先,在文件顶部添加以下导入:
import MobileCoreServices
这提供了对文件类型的访问。 接下来,在结构体的末尾添加以下内容:
// 1
var itemIdentifier: NSFileProviderItemIdentifier {
if isRoot {
return .rootContainer
} else {
return NSFileProviderItemIdentifier(
rawValue: urlRepresentation.dataRepresentation.base64EncodedString()
)
}
}
var isDirectory: Bool {
return urlRepresentation.hasDirectoryPath
}
var path: String {
return urlRepresentation.path
}
var containingDirectory: String {
return urlRepresentation.deletingLastPathComponent().path
}
var filename: String {
return urlRepresentation.lastPathComponent
}
// 2
var typeIdentifier: String {
guard !isDirectory else {
return kUTTypeFolder as String
}
let pathExtension = urlRepresentation.pathExtension
let unmanaged = UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension,
pathExtension as CFString,
nil
)
let retained = unmanaged?.takeRetainedValue()
return (retained as String?) ?? ""
}
// 3
var parentReference: MediaItemReference? {
guard !isRoot else {
return nil
}
return MediaItemReference(
urlRepresentation: urlRepresentation.deletingLastPathComponent()
)
}
记住:
- 1)
itemIdentifier
对于FileProvider
管理的每个项目必须是唯一的。 如果项引用是根,则它使用NSFileProviderItemIdentifier.rootContainer
。 如果没有,它会从引用的URL创建一个标识符。 - 2) 这里它根据URL的路径扩展创建一个标识符。 奇怪的
UTTypeCreatePreferredIdentifierForTag
是一个C函数,它返回给定输入的UTI
类型。 - 3) 在目录结构中工作时,引用父级是很有用的。 这表示包含当前引用的文件夹。 它是可选的,因为根引用没有父引用。
您在此处添加了一些其他属性,这些属性不需要太多解释,但在创建NSFileProviderItem
时非常有用。 这样,参考模型就完成了。 是时候在FileProviderItem
中hook
所有内容。
打开FileProviderItem.swift
并在文件顶部添加以下内容:
import FileProvider
然后,在类实现之外将以下内容添加到文件的底部:
// MARK: - NSFileProviderItem
extension FileProviderItem: NSFileProviderItem {
// 1
var itemIdentifier: NSFileProviderItemIdentifier {
return reference.itemIdentifier
}
var parentItemIdentifier: NSFileProviderItemIdentifier {
return reference.parentReference?.itemIdentifier ?? itemIdentifier
}
var filename: String {
return reference.filename
}
var typeIdentifier: String {
return reference.typeIdentifier
}
// 2
var capabilities: NSFileProviderItemCapabilities {
if reference.isDirectory {
return [.allowsReading, .allowsContentEnumerating]
} else {
return [.allowsReading]
}
}
// 3
var documentSize: NSNumber? {
return nil
}
}
FileProviderItem
现在符合NSFileProviderItem
并实现所有必需的属性。 仔细阅读代码:
- 1) 大多数必需属性只是映射您之前添加到
MediaItemReference
的逻辑。 - 2)
NSFileProviderItemCapabilities
指示可以对文档浏览器中的项目执行哪些操作 - 例如读取和删除。 对于这个应用程序,您只需要允许读取和枚举目录。 在实际用例中,您可能会使用.allowsAll
功能,因为用户希望所有操作都能正常工作。 - 3) 本教程不会使用文档大小,但它包含在内以防止
NSFileProviderManager.writePlaceholder(at:withMetadata :)
中的崩溃。 这可能是框架的一个bug
,幸运的是,一个典型的应用程序的File Extension
无论如何都会提供documentSize
。
这就是模型。 NSFileProviderItem
具有更多属性,但是您已实现的内容足以满足本教程的需要。
Enumerating Documents
现在该模型已经到位,是时候将它投入使用了。 要向用户显示模型定义的项目,您需要告知系统应用程序的内容。
NSFileProviderEnumerator
定义系统与应用程序内容之间的关系。 稍后,您将通过提供表示当前上下文的NSFileProviderItemIdentifier
来查看系统如何请求枚举器。 如果用户正在查看扩展的根目录,系统将提供.rootContainer
标识符。 当用户在目录内导航时,系统会传入模型定义的该项的标识符。
首先,您将构建启动项目中提供的枚举器。 打开Provider / FileProviderEnumerator.swift
并在path
下添加以下内容:
private var currentTask: URLSessionTask?
此属性将存储对当前网络任务的引用。 这提供了取消请求的能力。
接下来,使用以下内容替换enumerateItems(for:startingAt :)
的内容:
let task = NetworkClient.shared.getMediaItems(atPath: path) { results, error in
guard let results = results else {
let error = error ?? FileProviderError.noContentFromServer
observer.finishEnumeratingWithError(error)
return
}
let items = results.map { mediaItem -> FileProviderItem in
let ref = MediaItemReference(path: self.path, filename: mediaItem.name)
return FileProviderItem(reference: ref)
}
observer.didEnumerate(items)
observer.finishEnumerating(upTo: nil)
}
currentTask = task
这里,提供的网络客户端代码获取指定路径上的所有项目。 成功请求后,枚举器的观察者通过调用didEnumerate
然后使用finishEnumerating(upTo :)
来返回新数据,以指示批次项目的结束。 它通过调用finishEnumeratingWithError
通知枚举器的观察者是否从请求中发生错误。
注意:生产应用程序可能使用分页来获取数据。 它将使用
NSFileProviderPage
方法参数来执行此操作。 在这种情况下,应用程序将使用整数作为页面索引,然后将其序列化并存储在NSFileProviderPage
结构中。
完成枚举器的最后一步是将以下内容添加到invalidate()
:
currentTask?.cancel()
currentTask = nil
如果需要,这将取消当前的网络请求。 了解用户设备上的资源使用总是一个好主意,例如网络或位置访问。
完成该方法后,就可以使用此枚举器访问存储在后端的数据。 应用程序逻辑的其余部分将进入FileProviderExtension
类。
打开Provider / FileProviderExtension.swift
并用以下内容替换item(for :)
的内容:
guard let reference = MediaItemReference(itemIdentifier: identifier) else {
throw NSError.fileProviderErrorForNonExistentItem(withIdentifier: identifier)
}
return FileProviderItem(reference: reference)
系统提供传递给此方法的标识符,并返回该标识符的FileProviderItem
。 guard
语句确保标识符创建有效的MediaItemReference
。
接下来,使用以下代码替换urlForItem(withPersistentIdentifier :)
和persistentIdentifierForItem(at :)
:
// 1
override func urlForItem(withPersistentIdentifier
identifier: NSFileProviderItemIdentifier) -> URL? {
guard let item = try? item(for: identifier) else {
return nil
}
return NSFileProviderManager.default.documentStorageURL
.appendingPathComponent(identifier.rawValue, isDirectory: true)
.appendingPathComponent(item.filename)
}
// 2
override func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? {
let identifier = url.deletingLastPathComponent().lastPathComponent
return NSFileProviderItemIdentifier(identifier)
}
继续这个:
- 1) 验证项目以确保给定标识符解析为扩展模型的实例。 然后返回一个文件URL,指定将项目存储在文件管理器的文档存储目录中的位置。
- 2)
urlForItem(withPersistentIdentifier :)
返回的每个URL都需要映射回最初设置为表示的NSFileProviderItemIdentifier
。 在该方法中,您以
格式构建了URL,因此在这里您将采用倒数第二个路径组件作为项标识符。/ /
有两种方法需要您引用引用远程文件的文件占位符URL。 首先,您将创建一个帮助方法来创建此占位符。 将以下内容添加到providePlaceholder(at :)
:
// 1
guard
let identifier = persistentIdentifierForItem(at: url),
let reference = MediaItemReference(itemIdentifier: identifier)
else {
throw FileProviderError.unableToFindMetadataForPlaceholder
}
// 2
try fileManager.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true,
attributes: nil
)
// 3
let placeholderURL = NSFileProviderManager.placeholderURL(for: url)
let item = FileProviderItem(reference: reference)
// 4
try NSFileProviderManager.writePlaceholder(
at: placeholderURL,
withMetadata: item
)
下面就是在做的事情:
- 1) 首先,您从提供的
URL
创建标识符和引用。 如果失败,则抛出错误。 - 2) 创建占位符时,必须确保封闭目录存在,否则您将遇到问题。 因此,使用
NSFileManager
来执行此操作。 - 3) 传递给此方法的
url
用于显示图像,而不是占位符。 因此,您使用placeholderURL(for :)
创建占位符URL,并获取此占位符将表示的NSFileProviderItem
。 - 4) 将占位符项写入文件系统。
接下来,使用以下内容替换providePlaceholder(at:completionHandler :)
的内容:
do {
try providePlaceholder(at: url)
completionHandler(nil)
} catch {
completionHandler(error)
}
当需要占位符URL时,File Provider
将调用providePlaceholder(at:completionHandler :)
。 在其中,您尝试使用上面构建的帮助程序创建占位符,如果出现错误,则将其传递给completionHandler
。 成功之后,您无需传递任何内容 - File Provider
只需要您编写占位符URL,就像在providePlaceholder(at :)
中一样。
当用户导航目录时,File Provider
将调用enumerator(for:)
以请求给定标识符的FileProviderEnumerator
。 用以下内容替换此存根方法的内容:
if containerItemIdentifier == .rootContainer {
return FileProviderEnumerator(path: "/")
}
guard
let ref = MediaItemReference(itemIdentifier: containerItemIdentifier),
ref.isDirectory
else {
throw FileProviderError.notAContainer
}
return FileProviderEnumerator(path: ref.path)
此方法确保提供的标识符的项目是目录。 如果标识符是根项,则仍然创建枚举器,因为根是有效目录。
建立并运行。 应用启动后,切换到Files
应用并启用应用的扩展程序。 点击Browse
选项卡栏项两次以导航到Files
的根目录。 选择More Locations
并切换Provider
,这是扩展名称。
注意:如果找不到
More Locations
下列出的Provider
,请点击右上角的Edit
按钮,以确保列表中显示已禁用的扩展名。
您现在有一个有效的File Provider extension
! 缺少一些重要的东西,但接下来你会添加它们。
Providing Thumbnails
由于此应用程序应显示后端的图像,因此显示图像的缩略图非常重要。 有一种覆盖方法可以处理扩展的缩略图生成。
在enumerator(for:)
以下添加:
// MARK: - Thumbnails
override func fetchThumbnails(
for itemIdentifiers: [NSFileProviderItemIdentifier],
requestedSize size: CGSize,
perThumbnailCompletionHandler:
@escaping (NSFileProviderItemIdentifier, Data?, Error?) -> Void,
completionHandler: @escaping (Error?) -> Void)
-> Progress {
// 1
let progress = Progress(totalUnitCount: Int64(itemIdentifiers.count))
for itemIdentifier in itemIdentifiers {
// 2
let itemCompletion: (Data?, Error?) -> Void = { data, error in
perThumbnailCompletionHandler(itemIdentifier, data, error)
if progress.isFinished {
DispatchQueue.main.async {
completionHandler(nil)
}
}
}
guard
let reference = MediaItemReference(itemIdentifier: itemIdentifier),
!reference.isDirectory
else {
progress.completedUnitCount += 1
let error = NSError.fileProviderErrorForNonExistentItem(
withIdentifier: itemIdentifier
)
itemCompletion(nil, error)
continue
}
let name = reference.filename
let path = reference.containingDirectory
// 3
let task = NetworkClient.shared
.downloadMediaItem(named: name, at: path) { url, error in
guard
let url = url,
let data = try? Data(contentsOf: url, options: .alwaysMapped)
else {
itemCompletion(nil, error)
return
}
itemCompletion(data, nil)
}
// 4
progress.addChild(task.progress, withPendingUnitCount: 1)
}
return progress
}
虽然这种方法非常冗长,但其逻辑很简单:
- 1) 此方法返回一个
Progress
对象,该对象跟踪每个缩略图请求的状态。 - 2) 它为每个
itemIdentifier
定义一个完成块。 该块将负责调用此方法所需的每个项块以及最后调用最后一个块。 - 3) 缩略图文件使用启动器项目附带的
NetworkClient
从服务器下载到临时文件。 下载完成后,完成处理程序将下载的数据data
传递给itemCompletion
闭包。 - 4) 每个下载任务都作为依赖项添加到父进程对象。
注意:在处理较大的数据集时,为每个占位符发出单独的网络请求可能需要一些时间。 因此,如果可能,您的后端集成应提供在单个请求中批量下载图像的方法。
建立并运行。 在Files
中打开扩展名,您应该看到加载缩略图。
Viewing Items
现在,当您选择一个项目时,该应用会显示一个没有完整图像的空白视图:
到目前为止,您只实现了预览缩略图的显示 - 现在是时候添加查看完整内容的功能了!
与缩略图生成一样,查看项目内容只需要一种方法,即startProvidingItem(at:completionHandler :)
。 将以下内容添加到FileProviderExtension
类的底部:
// MARK: - Providing Items
override func startProvidingItem(
at url: URL,
completionHandler: @escaping ((_ error: Error?) -> Void)) {
// 1
guard !fileManager.fileExists(atPath: url.path) else {
completionHandler(nil)
return
}
// 2
guard
let identifier = persistentIdentifierForItem(at: url),
let reference = MediaItemReference(itemIdentifier: identifier)
else {
completionHandler(FileProviderError.unableToFindMetadataForItem)
return
}
// 3
let name = reference.filename
let path = reference.containingDirectory
NetworkClient.shared
.downloadMediaItem(named: name, at: path, isPreview: false) { fileURL, error in
// 4
guard let fileURL = fileURL else {
return completionHandler(error)
}
// 5
do {
try self.fileManager.moveItem(at: fileURL, to: url)
completionHandler(nil)
} catch {
completionHandler(error)
}
}
}
这是代码的作用:
- 1) 检查指定URL中是否已存在某个项目,以防止再次请求相同的数据。 在实际案例使用中,您应该检查修改日期和文件版本,以确保您拥有最新数据。 但是,在本教程中没有必要这样做,因为它不支持版本控制。
- 2) 获取关联URL的MediaItemReference,以便您知道需要从后端获取哪个文件。
- 3) 从引用中提取名称和路径以从后端请求文件内容。
- 4) 如果下载文件时出错,则会失败。
- 5) 将文件从其临时下载目录移动到扩展指定的文档存储URL。
建立并运行。 打开扩展程序后,选择一个项目以查看完整版本。
当您打开更多文件时,扩展程序将需要处理删除下载的文件。File Provider extension
内置了此功能。
您必须覆盖stopProvidingItem(at :)
以清理文件并为其提供新的占位符。 在FileProviderExtension
类的底部添加以下内容:
override func stopProvidingItem(at url: URL) {
try? fileManager.removeItem(at: url)
try? providePlaceholder(at: url)
}
这将删除该项,并调用providePlaceholder(at :)
以生成新的占位符。
这样就完成了File Provider
的最基本功能。 文件枚举,缩略图预览和查看文件内容是此扩展的基本组件。
恭喜, File Provider
完整且功能齐全!
了解可以在Apple’s Documentation about File Providers中为File Provider
实施的更多操作。 您还可以使用其他扩展程序将自定义UI添加到File Provider
,您可以在here阅读更多相关信息。
后记
本篇主要讲述了实现File Provider extension,感兴趣的给个赞或者关注~~~