Search code examples
swiftxcodecocoansdocumentnsopenpanel

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

  • 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