Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Document Creation with UIDocumentBrowserViewController

The documentation for documentBrowser(_:didRequestDocumentCreationWithHandler:) says, "Create a new document and save it to a temporary location. If you use a UIDocument subclass to create the document, you must close it before calling the importHandler block."

So I created a file URL by taking the URL for the user's temporary directory (FileManager.default.temporaryDirectory) and appending a name and extension (getting a path like "file:///private/var/mobile/Containers/Data/Application/C1DE454D-EA1E-4166-B137-5B43185169D8/tmp/Untitled.uti"). But when I call save(to:for:completionHandler:) passing this URL, the completion handler is never called back. I also tried using url(for:in:appropriateFor:create:) to pass a subdirectory in the user's temporary directory—the completion handler was still never called.

I understand the document browser view controller is managed by a separate process, which has its own read / write permissions. Beyond that though, I'm having a hard time understanding what the problem is. Where can new documents be temporarily saved so that the document browser process can move them?

Update: as of the current betas, I now see an error with domain NSFileProviderInternalErrorDomain and code 1 getting logged: "The reader is not permitted to access the URL." At least that's confirmation of what's happening…

like image 420
Erik Foss Avatar asked Jul 27 '17 05:07

Erik Foss


1 Answers

So, to start with, if you're using a custom UTI, it's got to be set up correctly. Mine look like this…

<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>CFBundleTypeIconFiles</key>
        <array>
            <string>icon-file-name</string> // Can be excluded, but keep the array
        </array>
        <key>CFBundleTypeName</key>
        <string>Your Document Name</string>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>LSHandlerRank</key>
        <string>Owner</string>
        <key>LSItemContentTypes</key>
        <array>
            <string>com.custom-uti</string>
        </array>
    </dict>
</array>

and

<key>UTExportedTypeDeclarations</key>
<array>
    <dict>
        <key>UTTypeConformsTo</key>
        <array>
            <string>public.data</string>  // My doc is saved as Data, not a file wrapper
        </array>
        <key>UTTypeDescription</key>
        <string>Your Document Name</string>
        <key>UTTypeIdentifier</key>
        <string>com.custom-uti</string>
        <key>UTTypeTagSpecification</key>
        <dict>
            <key>public.filename-extension</key>
            <array>
                <string>doc-extension</string>
            </array>
        </dict>
    </dict>
</array>

Also

<key>UISupportsDocumentBrowser</key>
<true/>

I subclass UIDocument as MyDocument and add the following method to create a new temp document…

static func create(completion: @escaping Result<MyDocument> -> Void) throws {

    let targetURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("Untitled").appendingPathExtension("doc-extension")

    coordinationQueue.async {
        let document = MyDocument(fileURL: targetURL)
        var error: NSError? = nil
        NSFileCoordinator(filePresenter: nil).coordinate(writingItemAt: targetURL, error: &error) { url in
            document.save(to: url, for: .forCreating) { success in
                DispatchQueue.main.async {
                    if success {
                        completion(.success(document))
                    } else {
                        completion(.failure(MyDocumentError.unableToSaveDocument))
                    }
                }
            }
        }
        if let error = error {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
        }
    }
}

Then init and display the DBVC as follows:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    lazy var documentBrowser: UIDocumentBrowserViewController = {
        let utiDecs = Bundle.main.object(forInfoDictionaryKey: kUTExportedTypeDeclarationsKey as String) as! [[String: Any]]
        let uti = utiDecs.first?[kUTTypeIdentifierKey as String] as! String
        let dbvc = UIDocumentBrowserViewController(forOpeningFilesWithContentTypes:[uti])

        dbvc.delegate = self
        return dbvc
    }()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = documentBrowser
        window?.makeKeyAndVisible()

        return true
    }
}

And my delegate methods are as follows:

func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler:    @escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Swift.Void) {

    do {
        try MyDocument.create() { result in
            switch result {
            case let .success(document):
                // .move as I'm moving a temp file, if you're using a template
                // this will be .copy 
                importHandler(document.fileURL, .move) 
            case let .failure(error):
                // Show error
                importHandler(nil, .none)
            }
        }
    } catch {
        // Show error
        importHandler(nil, .none)
    }
}

func documentBrowser(_ controller: UIDocumentBrowserViewController, didImportDocumentAt sourceURL: URL, toDestinationURL destinationURL: URL) {
    let document = MyDocument(fileURL: destinationURL)
    document.open { success in
        if success {
            // Modally present DocumentViewContoller for document
        } else {
            // Show error
        }
    }
}

And that's pretty much it. Let me know how you get on!

like image 117
Ashley Mills Avatar answered Oct 03 '22 17:10

Ashley Mills