Search code examples
iosiclouduidocumentutinsfilewrapper

Export UIDocument with custom file package UTI


I'm trying to export my UIDocument subclass with a UIDocumentPickerViewController. The subclass writes data to a FileWrapper and its UTI conforms to com.apple.package.

But the presented document picker shows "Documents in iCloud Drive are not available because the iCloud Drive setting is disabled."

The document is successfully written to the cache, as I can see from the exported container package.

Cache directory

When I change the document subclass and custom UTI to conform to a single file (e.g. public.plain-text), the document picker works fine and I can export the file. So the problem seems to be with the Document Type or Exported UTI.

Am I doing something wrong or is this a bug?


Info.plist

Document Type and Exported UTI

<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>CFBundleTypeIconFiles</key>
        <array/>
        <key>CFBundleTypeName</key>
        <string>Custom Doc</string>
        <key>LSHandlerRank</key>
        <string>Owner</string>
        <key>LSItemContentTypes</key>
        <array>
            <string>com.zxzxlch.documentsandbox.customdoc</string>
        </array>
        <key>LSTypeIsPackage</key>
        <true/>
    </dict>
</array>

...

<key>UTExportedTypeDeclarations</key>
<array>
    <dict>
        <key>UTTypeConformsTo</key>
        <array>
            <string>com.apple.package</string>
        </array>
        <key>UTTypeDescription</key>
        <string>Custom Doc File</string>
        <key>UTTypeIdentifier</key>
        <string>com.zxzxlch.documentsandbox.customdoc</string>
        <key>UTTypeTagSpecification</key>
        <dict>
            <key>public.filename-extension</key>
            <array>
                <string>zzz</string>
            </array>
        </dict>
    </dict>
</array>

CustomDocument.swift

private let textFilename = "contents.txt"

class CustomDocument: UIDocument {
    var content = "Test"

    override func load(fromContents contents: Any, ofType typeName: String?) throws {
        guard let topFileWrapper = contents as? FileWrapper,
            let textData = topFileWrapper.fileWrappers?[textFilename]?.regularFileContents else {
                return
        }
        content = String(data: textData, encoding: .utf8)!
    }

    override func contents(forType typeName: String) throws -> Any {
        let textFileWrapper = FileWrapper(regularFileWithContents: content.data(using: .utf8)!)
        textFileWrapper.preferredFilename = textFilename

        return FileWrapper(directoryWithFileWrappers: [textFilename: textFileWrapper])
    }

}

ViewController.swift

func exportDocument() {
    // Write to cache
    let cachesDir = FileManager.default.urls(for: FileManager.SearchPathDirectory.cachesDirectory, in: .allDomainsMask).first!
    let dataDir = cachesDir.appendingPathComponent("export", isDirectory: true)
    try! FileManager.default.createDirectory(at: dataDir, withIntermediateDirectories: true, attributes: nil)

    let fileURL = dataDir.appendingPathComponent("cookie").appendingPathExtension("zzz")

    let archive = CustomDocument(fileURL: fileURL)
    archive.content = "Cookie cat"

    archive.save(to: archive.fileURL, for: .forCreating) { success in
        guard success else {
            let alertController = UIAlertController.notice(title: "Cannot export data", message: nil)
            self.present(alertController, animated: true, completion: nil)
            return
        }

        let documentPicker = UIDocumentPickerViewController(url: archive.fileURL, in: .exportToService)
        documentPicker.delegate = self
        self.present(documentPicker, animated: true, completion: nil)
    }

}

Solution

  • This solves my problem: make the UTI also conform to public.composite-content, i.e.

    <key>UTTypeConformsTo</key>
    <array>
        <string>com.apple.package</string>
        <string>public.composite-content</string>
    </array>
    

    I'm not sure why though.