From 2da455bc2fadb50d4f7a049dd5f74ff5be3a02d8 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Tue, 26 Sep 2017 12:58:39 -0700 Subject: [PATCH] Replaces UIDocumentPickerViewController with UIDocumentBrowserViewController on iOS 11 --- .../GameCollectionViewController.swift | 11 +- .../Game Selection/GamesViewController.swift | 7 +- Delta/Importing/ImportController.swift | 172 ++++++++++++++---- 3 files changed, 150 insertions(+), 40 deletions(-) diff --git a/Delta/Game Selection/GameCollectionViewController.swift b/Delta/Game Selection/GameCollectionViewController.swift index 5608ee9..830b770 100644 --- a/Delta/Game Selection/GameCollectionViewController.swift +++ b/Delta/Game Selection/GameCollectionViewController.swift @@ -504,10 +504,12 @@ extension GameCollectionViewController: SaveStatesViewControllerDelegate /// ImportControllerDelegate extension GameCollectionViewController: ImportControllerDelegate { - func importController(_ importController: ImportController, didImportItemsAt urls: Set) + func importController(_ importController: ImportController, didImportItemsAt urls: Set, errors: [Error]) { guard let game = self._changingArtworkGame else { return } + var errors = errors + var imageURL: URL? if let url = urls.first @@ -533,7 +535,7 @@ extension GameCollectionViewController: ImportControllerDelegate } catch { - print(error) + errors.append(error) } } else @@ -542,6 +544,11 @@ extension GameCollectionViewController: ImportControllerDelegate } } + for error in errors + { + print(error) + } + if let imageURL = imageURL { // Remove previous artwork from cache. diff --git a/Delta/Game Selection/GamesViewController.swift b/Delta/Game Selection/GamesViewController.swift index 61b233f..a4fcb3e 100644 --- a/Delta/Game Selection/GamesViewController.swift +++ b/Delta/Game Selection/GamesViewController.swift @@ -283,8 +283,13 @@ extension GamesViewController: ImportControllerDelegate self.present(importController, animated: true, completion: nil) } - func importController(_ importController: ImportController, didImportItemsAt urls: Set) + func importController(_ importController: ImportController, didImportItemsAt urls: Set, errors: [Error]) { + for error in errors + { + print(error) + } + let gameURLs = urls.filter { $0.pathExtension.lowercased() != "deltaskin" } DatabaseManager.shared.importGames(at: Set(gameURLs)) { (games, errors) in if errors.count > 0 diff --git a/Delta/Importing/ImportController.swift b/Delta/Importing/ImportController.swift index 8de44e4..8147092 100644 --- a/Delta/Importing/ImportController.swift +++ b/Delta/Importing/ImportController.swift @@ -16,7 +16,7 @@ import Roxas protocol ImportControllerDelegate { - func importController(_ importController: ImportController, didImportItemsAt urls: Set) + func importController(_ importController: ImportController, didImportItemsAt urls: Set, errors: [Error]) /** Optional **/ func importControllerDidCancel(_ importController: ImportController) @@ -37,63 +37,91 @@ class ImportController: NSObject var delegate: ImportControllerDelegate? var importOptions: [ImportOption]? + fileprivate weak var presentingViewController: UIViewController? + + // 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. + fileprivate weak var presentedViewController: UIViewController? + + fileprivate let importQueue: OperationQueue + fileprivate 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() } - fileprivate weak var presentingViewController: UIViewController? - fileprivate func presentImportController(from presentingViewController: UIViewController, animated: Bool, completionHandler: ((Void) -> Void)?) { self.presentingViewController = presentingViewController -#if IMPACTOR - - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - alertController.addAction(UIAlertAction.cancel) - - if let importOptions = self.importOptions - { - for importOption in importOptions + #if IMPACTOR + + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction.cancel) + + if let importOptions = self.importOptions { - alertController.add(importOption, completionHandler: finish(with:)) + for importOption in importOptions + { + alertController.add(importOption) { [unowned self] (urls) in + self.finish(with: urls, errors: []) + } + } } - } - - self.presentingViewController?.present(alertController, animated: true, completion: nil) - -#else - - let documentMenuController = UIDocumentMenuViewController(documentTypes: Array(self.documentTypes), in: .import) - documentMenuController.delegate = self - - if let reversedImportOptions = self.importOptions?.reversed() - { - for importOption in reversedImportOptions + + self.presentedViewController = alertController + self.presentingViewController?.present(alertController, animated: true, completion: nil) + + #else + + let documentMenuController = UIDocumentMenuViewController(documentTypes: Array(self.documentTypes), in: .import) + documentMenuController.delegate = self + + if let reversedImportOptions = self.importOptions?.reversed() { - documentMenuController.add(importOption, order: .first, completionHandler: finish(with:)) + for importOption in reversedImportOptions + { + documentMenuController.add(importOption, order: .first) { [unowned self] (urls) in + self.finish(with: urls, errors: []) + } + } } - } - - self.presentingViewController?.present(documentMenuController, animated: true, completion: nil) -#endif - + + self.presentedViewController = documentMenuController + self.presentingViewController?.present(documentMenuController, animated: true, completion: nil) + + #endif } - fileprivate func finish(with urls: Set?) + @objc fileprivate func cancel() + { + self.finish(with: nil, errors: []) + } + + fileprivate func finish(with urls: Set?, errors: [Error]) { if let urls = urls { - self.delegate?.importController(self, didImportItemsAt: urls) + self.delegate?.importController(self, didImportItemsAt: urls, errors: errors) } else { self.delegate?.importControllerDidCancel(self) } + self.presentedViewController?.dismiss(animated: true) + self.presentingViewController?.importController = nil } } @@ -103,13 +131,32 @@ extension ImportController: UIDocumentMenuDelegate { func documentMenu(_ documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController) { - documentPicker.delegate = self - self.presentingViewController?.present(documentPicker, animated: true, completion: nil) + if #available(iOS 11.0, *) + { + let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(ImportController.cancel)) + + let documentBrowserViewController = UIDocumentBrowserViewController(forOpeningFilesWithContentTypes: Array(self.documentTypes)) + documentBrowserViewController.delegate = self + documentBrowserViewController.browserUserInterfaceStyle = .dark + documentBrowserViewController.allowsPickingMultipleItems = true + documentBrowserViewController.allowsDocumentCreation = false + documentBrowserViewController.additionalTrailingNavigationBarButtonItems = [cancelButton] + + self.presentedViewController = documentBrowserViewController + self.presentingViewController?.present(documentBrowserViewController, animated: true, completion: nil) + } + else + { + documentPicker.delegate = self + + self.presentedViewController = documentPicker + self.presentingViewController?.present(documentPicker, animated: true, completion: nil) + } } func documentMenuWasCancelled(_ documentMenu: UIDocumentMenuViewController) { - self.finish(with: nil) + self.finish(with: nil, errors: []) } } @@ -117,15 +164,66 @@ extension ImportController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { - self.finish(with: [url]) + self.finish(with: [url], errors: []) + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) + { + self.finish(with: Set(urls), errors: []) } func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { - self.finish(with: nil) + self.finish(with: nil, errors: []) } } +@available(iOS 11.0, *) +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() + + let intent = NSFileAccessIntent.readingIntent(with: url) + self.fileCoordinator.coordinate(with: [intent], queue: self.importQueue) { (error) in + if let error = error + { + errors.append(error) + } + else + { + let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent) + + do + { + // Always access intent.url, as the system may have updated it when requesting access. + try FileManager.default.copyItem(at: intent.url, to: temporaryURL) + + coordinatedURLs.insert(temporaryURL) + } + catch + { + errors.append(error) + } + } + + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: self.importQueue.underlyingQueue!) { + self.finish(with: coordinatedURLs, errors: errors) + } + } +} private var ImportControllerKey: UInt8 = 0