FileProvider框架详细解析 (三) —— 实现File Provider extension(二)

1,121 阅读3分钟
原文链接: www.jianshu.com

版本记录

版本号 时间
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,感兴趣的给个赞或者关注~~~