'NSDocumentController.openDocument not allowing selection of custom file type

I suspect this is a finicky configuration issue that I'm getting wrong with Document Type / UTI declarations in my Info.plist, but have tried several things and haven't found the solution.

I have an NSDocument-based Mac application, written in Swift, Xcode 11. It reads and writes a custom document type with suffix "mpxml".

enter image description here

During most early development I did not have a custom UTI identifier for the type (the identifier field was left blank, the project default), and the app was able to read and write these files.

Recently I changed to have a fully qualified document type identifier and editor, which seemed to be necessary to get my document icon to show up in the Finder. I changed all the places in my code referencing document type to use this fully qualified UTI. Everything now works except that the open panel (run by the default NSDocumentController openDocument) no longer recognizes my file type - all files with "mpxml" suffix are now grayed out in the open panel, including any files freshly created (the save panel works fine to write the documents).

Some things I've tried:

  • adding some additional overrides to my NSDocument subclass: fileNameExtension(), writableTypes, etc.
  • setting / omitting mime-type
  • setting / omitting a 4-char OSType
  • setting / omitting a reference URL
  • removing mpxml extension from document type (so it's only defined in the UTI) - didn't work
  • declaring the type as an Imported UTI as well (should not be needed, didn't fix the issue)
  • reviewing docs: Developing a Document-Based App, Declaring New Uniform Type Identifiers

Worth noting: the documentation on CFBundleTypeExtensions (the relevant document-type plist key) says that it's ignored if LSItemContentTypes is set - which is the case, since LSItemContentTypes is the key for the UTI identifier. But if setting this breaks the document-type suffix affiliation, I'd expect the UTI export affiliation to re-connnect it.

Also: Open Recent is also broken, and on attempting to open a recently-saved document the error reported is that the app "cannot open files of this type".

I'm not sure a workaround bypassing NSDocumentController will work here, because I don't want to mess with the document instance / window / file associations it sets up behind the scenes.

What is missing to make the custom UTI and extension work correctly in this app?


UPDATE based on request here is additional Info.plist data relevant to this bug (which essentially agrees with the XCode document type information in the screenshot above). I've now created a minimal sample application which reproduces the bug that I'll be using for an Apple bug report.

In the original form of the project, which no custom UTI declared, the Info.plist document type declaration is:

<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>CFBundleTypeExtensions</key>
        <array>
            <string>asdfg</string>
        </array>
        <key>CFBundleTypeIconFile</key>
        <string></string>
        <key>CFBundleTypeName</key>
        <string>example document</string>
        <key>CFBundleTypeOSTypes</key>
        <array>
            <string>CCdc</string>
        </array>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>LSTypeIsPackage</key>
        <integer>0</integer>
        <key>NSDocumentClass</key>
        <string>$(PRODUCT_MODULE_NAME).CCDocument</string>
    </dict>
</array>

This project is used to successfully read and write non-empty documents with the suffix .asdfg.

I then update the Info.plist by creating a custom UTI for this extension. At this point the Info.plist is as follows (document-type and UTI the only changes):

<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>CFBundleTypeExtensions</key>
        <array/>
        <key>CFBundleTypeIconFile</key>
        <string></string>
        <key>CFBundleTypeName</key>
        <string>ccutibug document</string>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>LSItemContentTypes</key>
        <array>
            <string>com.mathaesthetics.ccutibug</string>
        </array>
        <key>LSTypeIsPackage</key>
        <integer>0</integer>
        <key>NSDocumentClass</key>
        <string>$(PRODUCT_MODULE_NAME).CCDocument</string>
    </dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
    <dict>
        <key>UTTypeDescription</key>
        <string>ccutibug document</string>
        <key>UTTypeIdentifier</key>
        <string>com.mathaesthetics.ccutibug</string>
        <key>UTTypeTagSpecification</key>
        <dict>
            <key>com.apple.ostype</key>
            <array>
                <string>CCdc</string>
            </array>
            <key>public.filename-extension</key>
            <array>
                <string>asdfg</string>
            </array>
        </dict>
    </dict>
</array>

The same symptoms described in the original occur after this change to the minimal test project - Open Panel now has all .asdfg documents disabled, Open Recent no longer works, but I can still create and save these documents. The clean rebuild plus lsregister fix suggested by @catlan still does not correct it.

Again I can confirm by directly using an open panel that supplying the UTI alone does not enable open panel to honor the extension, only explicitly supplying the extension lets me open the saved documents, and there's no way to do that through NSDocumentController's handling of the open panel or open-recent menu AFAIK.



Solution 1:[1]

Your UTExportedTypeDeclarations entry is missing UTTypeConformsTo. This key is required. See Uniform Type Identifier Concepts - Conformance and Declaring New Uniform Type Identifiers.

Although a custom UTI can conform to any UTI, public.data or com.apple.package must be at the root of the conformance hierarchy for all custom UTIs that are file formats (such as documents);

Also:

You need to declare conformance only with your type’s immediate “superclass,” because the conformance hierarchy allows for inheritance between identifiers. That is, if you declare your identifier as conforming to the public.tiff identifier, it automatically conforms to identifiers higher up in the hierarchy, such as public.image and public.data.

System-Declared Uniform Type Identifiers

<key>UTExportedTypeDeclarations</key>
<array>
    <dict>
        <key>UTTypeConformsTo</key>
        <array>
            <string>public.data</string>
        </array>
        <key>UTTypeDescription</key>
        <string>ccutibug document</string>
        <key>UTTypeIdentifier</key>
        <string>com.mathaesthetics.ccutibug</string>
        <key>UTTypeTagSpecification</key>
        <dict>
            <key>public.filename-extension</key>
            <array>
                <string>asdfg</string>
            </array>
        </dict>
    </dict>
</array>

I also removed com.apple.ostype, which was used in classic Mac OS and is not required for new file types.

And match public.filename-extension and CFBundleTypeExtensions:

<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>CFBundleTypeExtensions</key>
        <array>
            <string>asdfg</string>
        </array>
        <key>CFBundleTypeIconFile</key>
        <string></string>
        <key>CFBundleTypeName</key>
        <string>ccutibug document</string>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>LSItemContentTypes</key>
        <array>
            <string>com.mathaesthetics.ccutibug</string>
        </array>
        <key>LSTypeIsPackage</key>
        <false/>
        <key>NSDocumentClass</key>
        <string>$(PRODUCT_MODULE_NAME).CCDocument</string>
    </dict>
</array>

Note: I also changed <integer>0</integer> to <false/> to make it more readable.

Debugging

During development changing the UTI can confuse the LaunchServices database. You can try to reset it by running:

/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user

Note: Make sure that you don't have any old dev build on your system, like in the Xcode Build Folder or Xcode Product Archives. These could continue to confuse the LaunchServices database

The -dump option is helpful to see current UTI declarations:

/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -dump

Solution 2:[2]

I had the same problem developing my first SwiftUI App based application. The NSDocumentController open command would not allow me to select my custom document type. I found a solution which is in agreement with Apples documentation on custom document types. The problem with this documentation is that you find often outdated documentation as well and I tended to mix all the information.

The key is - as mentioned - to define a correct "Exported Type Identifier". In my case I had the additional problem that my document format is a package which needs to be recognised by the system as a package.

The decisive thing is to choose the correct "Conforms To" entry (precisely correct!). It needs to be one of the system declared uniform type identifiers (see documentation here). For me this was "com.apple.package". Interesting is that the file extension is only present in the "Exported Type Identifier". After I had done this the NSDocumentController open command allowed me to select the correct files (which were directories correctly recognised as packages).

Here the complete entry: Declaring a custom document type and the associated Exported Type Identifier

Solution 3:[3]

It's 2022, Xcode 12, and SwiftUI, and I've run into the same thing, and like the OP I've tried everything I could think of and then some.

Here's what works for me: – In the Info settings, I filled out the document type and the ExportedType identifiers.

Important: – I'd originally added the ImportedType to my plist and had to delete it for this to work.

– I declared a UTType:

extension UTType {
    static var myExtension: UTType {
       UTType(exportedAs: "com.MyDomain.myAppName")
    }
}

(if you use the importedAs constructor here it will still work, but the compiler - rightly - complains)

and added static var readableContentTypes: [UTType] { [.myExtension] } to my document.

read, write, restoring windows on running the app: everything works automagically.

Solution 4:[4]

After verifying that all the settings seem correct, I was able to workaround the NSDocumentController issues by just implementing openDocument() and application(openFile...) in my app delegate.

For the open panel:

@IBAction func openDocument(_ sender: Any?) {
    let openPanel = NSOpenPanel()
    openPanel.canChooseFiles = true
    openPanel.allowsMultipleSelection = false
    openPanel.canChooseDirectories = false

    // here we set both custom UTI and suffix explicitly as allowed file types
    openPanel.allowedFileTypes = [MPDocument.UTI, MPDocument.mpxml_suffix]
    openPanel.allowsOtherFileTypes = false

    openPanel.begin(completionHandler: { repsonse in 
        if let docURL = openPanel.url {

            do {
                let mpdoc = try MPDocument(contentsOf: docURL, ofType: MPDocument.UTI)
                NSDocumentController.shared.addDocument(mpdoc)
                mpdoc.makeWindowControllers()
                mpdoc.showWindows()
                if let newDocWC = mpdoc.windowControllers.first {
                    newDocWC.window?.makeKeyAndOrderFront(nil)
                }
            } catch {
                os_log(.error, "Could not load file from URL - error: %s", String(describing:error))
            }
        }
    })
}

Open Recent would still fail (unsupported document type), so this AppDelegate override corrects that by coercing type to the custom UTI:

func application(_ sender: NSApplication, openFile filename: String) -> Bool {
    var result = false
    let docURL = URL(fileURLWithPath: filename)
    do {
        // for Open Recent - specify type explicitly rather than relying on suffix
        let mpdoc = try MPDocument(contentsOf: docURL, ofType: MPDocument.UTI)
        NSDocumentController.shared.addDocument(mpdoc)
        mpdoc.makeWindowControllers()
        mpdoc.showWindows()
        if let newDocWC = mpdoc.windowControllers.first {
            newDocWC.window?.makeKeyAndOrderFront(nil)
            result = true
        }
    } catch {
        os_log(.error, "Could not load file from URL - error: %s", String(describing:error))
    }
    return result
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 M Wilm
Solution 3 green_knight
Solution 4 Corbell