// // ImportController.swift // Delta // // Created by Riley Testut on 10/10/15. // Copyright © 2015 Riley Testut. All rights reserved. // import UIKit import MobileCoreServices import ObjectiveC import DeltaCore import Roxas protocol ImportControllerDelegate { func importController(_ importController: ImportController, didImportItemsAt urls: Set, errors: [Error]) /** Optional **/ func importControllerDidCancel(_ importController: ImportController) } extension ImportControllerDelegate { func importControllerDidCancel(_ importController: ImportController) { // Empty Implementation } } class ImportController: NSObject { let documentTypes: Set var delegate: ImportControllerDelegate? var importOptions: [ImportOption]? weak var presentingViewController: UIViewController? weak var barButtonItem: UIBarButtonItem? weak var sourceView: UIView? // Store presentedViewController separately, since when we dismiss we don't know if it has already been dismissed. // Calling dismiss on presentingViewController in that case would dismiss presentingViewController, which is bad. private weak var presentedViewController: UIViewController? private let importQueue: OperationQueue private let fileCoordinator: NSFileCoordinator init(documentTypes: Set) { self.documentTypes = documentTypes let dispatchQueue = DispatchQueue(label: "com.rileytestut.Delta.ImportController.dispatchQueue", qos: .userInitiated, attributes: .concurrent) self.importQueue = OperationQueue() self.importQueue.name = "com.rileytestut.Delta.ImportController.importQueue" self.importQueue.underlyingQueue = dispatchQueue self.fileCoordinator = NSFileCoordinator(filePresenter: nil) super.init() } func makeActions() -> [Action] { assert(self.presentingViewController != nil, "presentingViewController must be set before calling makeActions()") var actions = (self.importOptions ?? []).map { (option) -> Action in let action = Action(title: option.title, style: .default, image: option.image) { _ in option.import { importedURLs in self.finish(with: importedURLs, errors: []) } } return action } let filesAction = Action(title: NSLocalizedString("Files", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "doc")) { action in self.presentDocumentBrowser() } actions.append(filesAction) return actions } fileprivate func presentImportController(from presentingViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) { self.presentingViewController = presentingViewController let actions = self.makeActions() if actions.count > 1 { let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) alertController.addAction(UIAlertAction.cancel) let alertActions = actions.map { UIAlertAction($0) } for action in alertActions { alertController.addAction(action) } if let sourceView = self.sourceView { alertController.popoverPresentationController?.sourceView = sourceView.superview alertController.popoverPresentationController?.sourceRect = sourceView.frame } else { alertController.popoverPresentationController?.barButtonItem = self.barButtonItem } self.presentedViewController = alertController self.presentingViewController?.present(alertController, animated: true, completion: nil) } else { self.presentDocumentBrowser() } } @objc private func cancel() { self.finish(with: nil, errors: []) } private func finish(with urls: Set?, errors: [Error]) { DispatchQueue.main.async { if let urls = urls { self.delegate?.importController(self, didImportItemsAt: urls, errors: errors) } else { self.delegate?.importControllerDidCancel(self) } self.presentedViewController?.dismiss(animated: true) self.presentingViewController?.importController = nil } } private func presentDocumentBrowser() { let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(ImportController.cancel)) let documentBrowserViewController = UIDocumentBrowserViewController(forOpeningFilesWithContentTypes: Array(self.documentTypes)) documentBrowserViewController.delegate = self documentBrowserViewController.modalPresentationStyle = .fullScreen documentBrowserViewController.browserUserInterfaceStyle = .dark documentBrowserViewController.allowsPickingMultipleItems = true documentBrowserViewController.allowsDocumentCreation = false documentBrowserViewController.additionalTrailingNavigationBarButtonItems = [cancelButton] self.presentedViewController = documentBrowserViewController self.presentingViewController?.present(documentBrowserViewController, animated: true, completion: nil) } } extension ImportController { func importExternalFile(at fileURL: URL, completionHandler: @escaping (Result) -> Void) { let intent = NSFileAccessIntent.readingIntent(with: fileURL) self.fileCoordinator.coordinate(with: [intent], queue: self.importQueue) { (error) in do { if let error = error { throw error } else { // User intent.url, not url, as the system may have updated it when requesting access. guard intent.url.startAccessingSecurityScopedResource() else { throw CocoaError.error(.fileReadNoPermission) } defer { intent.url.stopAccessingSecurityScopedResource() } // Use url, not intent.url, to ensure the file name matches what was in the document browser. let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileURL.lastPathComponent) try FileManager.default.copyItem(at: intent.url, to: temporaryURL, shouldReplace: true) completionHandler(.success(temporaryURL)) } } catch { completionHandler(.failure(error)) } } } } extension ImportController: UIDocumentBrowserViewControllerDelegate { func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentURLs documentURLs: [URL]) { var coordinatedURLs = Set() var errors = [Error]() let dispatchGroup = DispatchGroup() for url in documentURLs { dispatchGroup.enter() self.importExternalFile(at: url) { (result) in switch result { case .failure(let error): errors.append(error) case .success(let fileURL): coordinatedURLs.insert(fileURL) } dispatchGroup.leave() } } dispatchGroup.notify(queue: self.importQueue.underlyingQueue!) { self.finish(with: coordinatedURLs, errors: errors) } } } private var ImportControllerKey: UInt8 = 0 extension UIViewController { fileprivate var importController: ImportController? { set { objc_setAssociatedObject(self, &ImportControllerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } get { return objc_getAssociatedObject(self, &ImportControllerKey) as? ImportController } } func present(_ importController: ImportController, animated: Bool, completion: (() -> Void)?) { self.importController = importController importController.presentImportController(from: self, animated: animated, completionHandler: completion) } }