GBA002/Delta/Importing/ImportController.swift

251 lines
8.5 KiB
Swift

//
// 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<URL>, errors: [Error])
/** Optional **/
func importControllerDidCancel(_ importController: ImportController)
}
extension ImportControllerDelegate
{
func importControllerDidCancel(_ importController: ImportController)
{
// Empty Implementation
}
}
class ImportController: NSObject
{
let documentTypes: Set<String>
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<String>)
{
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<URL>?, 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<URL, Error>) -> 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<URL>()
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)
}
}