- 原文地址:iOS File Provider Extension Tutorial
- 原文做者:Ryan Ackermann
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:iWeslie
- 校對者:swants
在本教程中,你將學習 File Provider 拓展以及如何使用它把你 App 的內容公開出來。html
File Provider 在 iOS 11 中引入,它經過 iOS 的 文件 App 來訪問你 App 管理的內容。同時其餘的 App 也可使用 UIDocumentBrowserViewController
或 UIDocumentPickerViewController
來訪問你 App 的數據。前端
File Provider 拓展的主要任務是:android
你將使用 Heroku 按鈕 來配置託管文件的服務器。在服務器設置完成後,你須要配置擴展來對服務器的內容進行枚舉。ios
首先,請先 下載資源,完成後找到 Favart-Starter 文件夾並打開 Favart.xcodeproj。確保你已選擇 Favart 的 scheme,而後編譯並運行該 App,你會看到如下內容:git
該 App 提供了一個基礎的 View 來告訴用戶如何啓用 File Provider 擴展,由於你實際上不會在 App 內執行任何操做。每次在本教程中編譯運行 App 時,你都將返回主屏幕並打開 文件 這個 App 來訪問你的擴展。github
注意:若是要在真機上運行該項目,除了爲兩個 target 設置開發者信息外,還須要在 Configuration 文件夾中編輯 Favart.xcconfig。將 Bundle ID 更新爲惟一值。swift
示例項目將這個值用於兩個 target 中 build setting 裏的
PRODUCT_BUNDLE_IDENTIFIER
,Provider.entitlements 裏的 App Groups 標識符,還有 Info.plist 中的NSExtensionFileProviderDocumentGroup
。在項目中若是沒有同步更新它們,你將會獲得模糊而且讓人無法調試的編譯報錯信息,而使用自定義的 build settings 將會是一個聰明的方法。後端
示例項目中已經包含了你將用於 File Provider 擴展的基本組件:api
首先,你須要一個本身的後端服務器實例。幸運的是,使用 Heroku Button 將很容易完成這個操做。單擊下面的按鈕訪問 Heroku 的 dashboard。xcode
在你註冊完 Heroku 的免費帳號後,你將看到如下頁面:
在此頁面上,你能夠給你的 App 取一個名字,也能夠將該字段留空,Heroku 將爲你自動生成一個名稱。沒必要配置其餘東西,如今你能夠點擊 Deploy app 按鈕,一下子以後你的後端就會啓動並運行。
在 Heroku 完成部署 App 以後,單擊底部的 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 所依賴的模型。
首先,File Provider 須要一個遵循了 NSFileProviderItem
協議的模型。此模型將提供有關 File Provider 所管理的文件的信息。starter 項目在 FileProviderItem.swift 中已經定義了 FileProviderItem
,在使用它以前須要遵循一些協議。
雖然該協議含有 27 個屬性,但咱們只須要其中 4 個。其餘一些可選屬性爲 File Provider 提供有關每一個文件的詳細信息以及一些其餘功能。在本教程中,你將用到以四個屬性:itemIdentifier
、parentItemIdentifier
、filename
和 typeIdentifier
。
itemIdentifier
給模型提供了惟一標示符。File Provider 使用 parentIdentifier
來跟蹤它在擴展的層次結構中的位置。
filename
是 文件 裏顯示的 App 名字。typeIdentifier
是一個 統一類型標識符(UTI)。
在 FileProviderItem
能夠遵循 NSFileProviderItem
協議以前,它還須要一個處理來自後端數據的方法。MediaItem
定義了一個後端數據的簡單模型。咱們並非直接在 FileProviderItem
中使用這個模型,而是使用 MediaItemReference
來處理 File Provider 擴展的一些額外邏輯從而把其中的坑填上。
你將在本教程中使用 MediaItemReference
有兩個緣由:
NSFileProviderItem
所需的全部信息,所以你須要在其餘地方獲取它。爲了將教程的重心放到 File Provider 擴展自己上,你將使用 MediaItemReference
來快速入門,你須要將四個必填字段嵌入到 URL
對象中。而後將該 URL 編碼成 NSFilProviderItemIdentifier
。你不須要手動存儲其餘東西,由於 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)")!)
}
複製代碼
如下是代碼的詳解:
NSFileProviderItem
所需的大部分信息。在添加最終初的始化器以前,請把文件頂部的 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)
}
複製代碼
大部分擴展都將使用此初始化器。注意開頭的 itemReference://
。你能夠單獨處理根目錄的標識符以確保能正確設置其 URL 的路徑。
對於其餘項,你能夠將標識符的原始值轉換爲 base64 編碼後的數據來檢索 URL。URL 中的信息來自第一次對實例進行枚舉的網絡請求。
既然如今初始化器已經設置好了,是時候爲這個模型添加一些屬性了。首先,在文件頂部添加以下 import
:
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())
}
複製代碼
你須要知道記住如下幾點:
itemIdentifier
必須是惟一的。若是是根目錄,那麼它使用 NSFileProviderItemIdentifier.rootContainer
,不然從 URL 建立一個標識符。UTTypeCreatePreferredIdentifierForTag
其實是一個返回給定輸入的 UTI 類型的 C 函數。你在此處添加了一些其餘屬性,這些屬性不須要太多解釋,但在建立 NSFileProviderItem
時很是有用。如今參考模型已經建立完成了,是時候把全部東西與 FileProviderItem
進行掛鉤了。
打開 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
並實現了全部必須的屬性。以上代碼的詳解以下:
MediaItemReference
的邏輯。NSFileProviderItemCapabilities
表示能夠對文檔瀏覽器中的項目執行哪些操做,例如讀取和刪除。對於該 App,你只須要容許讀取和枚舉目錄。在實際項目中,你可能會使用 .allowsAll
,由於用戶但願全部操做均可以進行。NSFileProviderManager.writePlaceholder(at:withMetadata:)
會崩潰。這多是框架的一個錯誤,可是通常狀況下 App 的文件擴展不管如何都會提供 documentSize
。以上就是模型,NSFileProviderItem
還有更多其餘屬性,可是你目前實現的已經足夠了。
如今模型已經完善好了,能夠拿來使用了。你須要告訴系統你 App 裏有什麼內容才能向用戶展現模型定義的 item。
NSFileProviderEnumerator
定義系統和 App 內容間的關係。你稍後將看到系統是如何經過提供表示當前上下文的 NSFileProviderItemIdentifier
從而請求枚舉器的。若是用戶當前在根目錄下,系統將會提供 .rootContainer
標識符。在其餘目錄下時,系統則會傳入你模型定義的項目的標識符。
首先,在 starter 裏構建枚舉器。打開 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
複製代碼
這裏實現了 NetworkClient 單例獲取指定路徑的內容。請求成功後,枚舉器的觀察者經過調用 didEnumerate
和 finishEnumerating(upTo:)
來返回新的數據。經過 finishEnumeratingWithError
來通知枚舉器的觀察者請求到的結果是否有錯誤。
注意:實際的 App 可能使用分頁來獲取數據,這就會用到
NSFileProviderPage
來執行此操做。首先 App 將使用整數做爲頁面索引,而後將其序列化並存儲在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)
複製代碼
系統會提供 identifier 參數,而且你須要給那個 identifier 返回一個 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)
}
複製代碼
如下是代碼詳解:
urlForItem(withPersistentIdentifier:)
返回的每一個 URL 都須要映射回最初設置的 NSFileProviderItemIdentifier
。在該方法中,你要以 <documentStorageURL>/<itemIdentifier>/<filename>
的格式構建 URL 並採用 <itemIdentifier>
做爲標識符。如今有兩個方法都須要你傳入一個指向遠端文件的佔位符 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)
複製代碼
以上代碼完成的功能以下:
NSFileManager
來執行此操做。url
參數是用來顯示圖像的,而不是佔位符。所以你要使用 placeholderURL(for:)
來建立佔位符 URL,並獲取此佔位符將表示的 NSFileProviderItem
。接下來把 providePlaceholder(at:completionHandler:)
的內容替換成:
do {
try providePlaceholder(at: url)
completionHandler(nil)
} catch {
completionHandler(error)
}
複製代碼
當 File Provider 須要一個佔位符 URL 時,它將調用 providePlaceholder(at:completionHandler:)
。你將嘗試使用上面的輔助方法建立佔位符,若是拋出錯誤,則將其傳遞給 completionHandler
。就像在 providePlaceholder(at:)
中同樣,這個步驟成功以後就不須要傳遞任何內容,File Provider 只須要你的佔位符 URL。
當用戶在目錄下切換時,File Provider 將調用 enumerator(for:)
來請求給定 identifier 的 FileProviderEnumerator
。用如下內容替換該法的內容:
if containerItemIdentifier == .rootContainer {
return FileProviderEnumerator(path: "/")
}
guard let ref = MediaItemReference(itemIdentifier: containerItemIdentifier), ref.isDirectory
else {
throw FileProviderError.notAContainer
}
return FileProviderEnumerator(path: ref.path)
複製代碼
此方法確保了給定 identifier 對應的是一個目錄。若是是根目錄,則仍然建立枚舉器,由於根目錄也是有效目錄。
編譯並運行,App 啓動後,打開 文件 App,點擊兩次右下角的 瀏覽,你就會進入 文件 的根目錄。選擇 更多位置,會出現 提供者 或展開一個列表,點擊開啓你 App 的拓展。
注意:若是找不到 更多位置 展開的項目不能點擊,你能夠再點擊一下右上角的 編輯 按鈕。
你如今有一個有效的 File Provider 擴展了,可是還缺乏一些重要的東西,接下來你將添加它們。
由於 App 會顯示後端請求來的圖片,所以顯示出圖像的縮略圖很是重要,你能夠重寫一個方法來生成縮略圖。
在 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
}
複製代碼
雖然這種方法很是冗長,但其邏輯很簡單:
Progress
對象,該對象會記錄每一個縮略圖請求的狀態。itemIdentifier
定義了一個 completion 閉包,該閉包將負責調用此方法所需的每一個項的閉包以及最後調用最後一個閉包。NetworkClient
將縮略圖文件從服務器下載到臨時文件。在下載完成後,completion handler 將下載的 data
傳遞給 itemCompletion
閉包。注意:在處理較大的數據時,爲每一個佔位符都發出單獨的網絡請求可能須要耗費一些時間。所以若是可能的話,你的後端應提供單個請求中的批量下載圖像方法。
編譯並運行。打開 文件 裏的拓展就能看到你的縮略圖了:
如今當你選擇一個項目時,該 App 將會顯示一個沒有完整圖像的空白視圖:
到目前爲止,你只實現了預覽縮略圖的顯示,還須要添加完整圖片的顯示。
與縮略圖生成同樣,讓完整的圖片顯示只須要一個方法,即 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 {
completionHandler(error)
return
}
// 5
do {
try self.fileManager.moveItem(at: fileURL, to: url)
completionHandler(nil)
} catch {
completionHandler(error)
}
}
}
複製代碼
以上的代碼功能是:
URL
的 MediaItemReference
來確認須要從後端請求哪一個文件。編譯並運行,打開擴展後選擇任何一張圖,你能夠看到完整的圖片。
當你打開更多文件時,該擴展程序須要刪除已經下載了的文件,File Provider 擴展內置了這個功能。
你必須重寫 stopProvidingItem(at:)
,這樣才能清理下載了的文件並提供新的佔位符。在 FileProviderExtension
類的底部添加如下內容:
override func stopProvidingItem(at url: URL) {
try? fileManager.removeItem(at: url)
try? providePlaceholder(at: url)
}
複製代碼
這樣就能刪除圖片,並調用 providePlaceholder(at:)
來生成一個新的佔位符。
以上就完成了 File Provider 的最基本功能。文件枚舉,縮略圖預覽以及查看文件內容是此擴展的基本組件。
到如今爲止,你的 File Provider 的功能就齊全了。
你如今已經擁有了一個包含了有效的 File Provider 的 App,這個擴展程序能夠枚舉以及顯示後端服務器的東西。
你能夠點擊 下載資源 來下載完整版的項目。
你能夠在 Apple 關於 File Provider 的文檔 中瞭解更多有關 File Provider 的操做。你還可使用其餘擴展程序將自定義 UI 添加到 File Provider,你能夠從 這裏 能夠閱讀到更多相關信息。
若是你對其餘在 iOS 上使用文件的操做感興趣,你能夠查看 的更多方式感興趣,請查看 基於文檔的 App。
但願你喜歡這個教程!若是你有任何問題或意見,能夠加入 原文 最下面的討論組。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。