版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2019.05.16 星期四 |
前言
今天翻阅苹果的API文档,发现多了一个框架就是FileProvider,看了下才看见是iOS11.0新添加的框架,这里我们就一起来看一下框架FileProvider。感兴趣的看下面几篇文章。
1. FileProvider框架详细解析 (一) —— 基本概览(一)
2. FileProvider框架详细解析 (二) —— 实现File Provider extension(一)
源码
1. Swift
首先看下文档组织结构

下面看下sb中的内容

下面就是看源码了
1. NetworkClient.swift
import Foundation
final class NetworkClient {
static let shared = NetworkClient()
private enum APIConfig {
enum Path: String {
case media, file, preview
}
enum Parameter: String {
case id, path
}
}
private let session: URLSession = .shared
private func buildURL(with path: APIConfig.Path, queryItems: [URLQueryItem]) -> URL {
var components = URLComponents()
components.scheme = "https"
components.host = "aqueous-hamlet-86046.herokuapp.com"
components.path = "/" + path.rawValue
components.queryItems = queryItems
return components.url!
}
@discardableResult
func getMediaItems(atPath path: String = "/",
handler: @escaping ([MediaItem]?, Error?) -> Void) -> URLSessionTask {
let url = buildURL(with: .media, queryItems: [
URLQueryItem(name: APIConfig.Parameter.path.rawValue, value: path)
])
let task = session.dataTask(with: url) { data, _, error in
guard
let data = data,
let results = try? JSONDecoder().decode([MediaItem].self, from: data)
else {
return handler(nil, error)
}
handler(results, nil)
}
task.resume()
return task
}
@discardableResult
func downloadMediaItem(named name: String,
at path: String,
isPreview: Bool = true,
handler: @escaping (URL?, Error?) -> Void) -> URLSessionTask {
let url = buildURL(with: isPreview ? .preview : .file, queryItems: [
URLQueryItem(name: APIConfig.Parameter.id.rawValue, value: name),
URLQueryItem(name: APIConfig.Parameter.path.rawValue, value: path)
])
let task = session.downloadTask(with: url) { url, _, error in
handler(url, error)
}
task.resume()
return task
}
}
2. FileProviderExtension.swift
import FileProvider
class FileProviderExtension: NSFileProviderExtension {
private lazy var fileManager = FileManager()
override func item(for identifier: NSFileProviderItemIdentifier) throws -> NSFileProviderItem {
guard let reference = MediaItemReference(itemIdentifier: identifier) else {
throw NSError.fileProviderErrorForNonExistentItem(withIdentifier: identifier)
}
return FileProviderItem(reference: reference)
}
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)
}
override func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? {
let identifier = url.deletingLastPathComponent().lastPathComponent
return NSFileProviderItemIdentifier(identifier)
}
private func providePlaceholder(at url: URL) throws {
guard
let identifier = persistentIdentifierForItem(at: url),
let reference = MediaItemReference(itemIdentifier: identifier)
else {
throw FileProviderError.unableToFindMetadataForPlaceholder
}
try fileManager.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true,
attributes: nil
)
let placeholderURL = NSFileProviderManager.placeholderURL(for: url)
let item = FileProviderItem(reference: reference)
try NSFileProviderManager.writePlaceholder(
at: placeholderURL,
withMetadata: item
)
}
override func providePlaceholder(at url: URL, completionHandler: @escaping (Error?) -> Void) {
do {
try providePlaceholder(at: url)
completionHandler(nil)
} catch {
completionHandler(error)
}
}
// MARK: - Enumeration
override func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
if containerItemIdentifier == .rootContainer {
return FileProviderEnumerator(path: "/")
}
guard
let ref = MediaItemReference(itemIdentifier: containerItemIdentifier),
ref.isDirectory
else {
throw FileProviderError.notAContainer
}
return FileProviderEnumerator(path: ref.path)
}
// MARK: - Thumbnails
override func fetchThumbnails(
for itemIdentifiers: [NSFileProviderItemIdentifier],
requestedSize size: CGSize,
perThumbnailCompletionHandler: @escaping (NSFileProviderItemIdentifier, Data?, Error?) -> Void,
completionHandler: @escaping (Error?) -> Void)
-> Progress {
let progress = Progress(totalUnitCount: Int64(itemIdentifiers.count))
for itemIdentifier in itemIdentifiers {
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
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)
}
progress.addChild(task.progress, withPendingUnitCount: 1)
}
return progress
}
// MARK: - Providing Items
override func startProvidingItem(at url: URL, completionHandler: @escaping ((_ error: Error?) -> Void)) {
guard !fileManager.fileExists(atPath: url.path) else {
completionHandler(nil)
return
}
guard
let identifier = persistentIdentifierForItem(at: url),
let reference = MediaItemReference(itemIdentifier: identifier)
else {
completionHandler(FileProviderError.unableToFindMetadataForItem)
return
}
let name = reference.filename
let path = reference.containingDirectory
NetworkClient.shared.downloadMediaItem(named: name, at: path, isPreview: false) { fileURL, error in
guard let fileURL = fileURL else {
return completionHandler(error)
}
do {
try self.fileManager.moveItem(at: fileURL, to: url)
completionHandler(nil)
} catch {
completionHandler(error)
}
}
}
override func stopProvidingItem(at url: URL) {
try? fileManager.removeItem(at: url)
try? providePlaceholder(at: url)
}
}
3. FileProviderEnumerator.swift
import FileProvider
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
private let path: String
private var currentTask: URLSessionTask?
init(path: String) {
self.path = path
super.init()
}
func invalidate() {
currentTask?.cancel()
currentTask = nil
}
func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
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
}
}
4. FileProviderError.swift
import Foundation
enum FileProviderError: Error {
case unableToFindMetadataForPlaceholder
case unableToFindMetadataForItem
case notAContainer
case unableToAccessSecurityScopedResource
case invalidParentItem
case noContentFromServer
}
5. FileProviderItem.swift
import FileProvider
final class FileProviderItem: NSObject {
let reference: MediaItemReference
init(reference: MediaItemReference) {
self.reference = reference
super.init()
}
}
// MARK: - NSFileProviderItem
extension FileProviderItem: NSFileProviderItem {
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
}
var capabilities: NSFileProviderItemCapabilities {
if reference.isDirectory {
return [.allowsReading, .allowsContentEnumerating]
} else {
return [.allowsReading]
}
}
var documentSize: NSNumber? {
return nil
}
}
6. MediaItem.swift
import Foundation
struct MediaItem: Codable {
let name: String
let size: Int?
}
7. MediaItemReference.swift
import FileProvider
import MobileCoreServices
struct MediaItemReference {
private let urlRepresentation: URL
private var isRoot: Bool {
return urlRepresentation.path == "/"
}
private init(urlRepresentation: URL) {
self.urlRepresentation = urlRepresentation
}
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)")!)
}
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)
}
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
}
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?) ?? ""
}
var parentReference: MediaItemReference? {
guard !isRoot else {
return nil
}
return MediaItemReference(
urlRepresentation: urlRepresentation.deletingLastPathComponent()
)
}
}
后记
本篇主要讲述了实现File Provider extension,感兴趣的给个赞或者关注~~~
