I had some time to work through this in depth today. As a reminder, my golden rule for this sort of thing is to do what Xcode does. So, I started out by creating a new Xcode project that works. Here’s what I did:
-
Using Xcode 16.3 on macOS 15.4, I created a new project, named Test783646, from the macOS > Document App template.
-
That has an asset catalogue, Assets.xcassets, that has an AppIcon placeholder. I filled that out with all the sizes.
-
At the bottom left of the asset catalogue editor there’s a plus (+) button. I clicked that and chose macOS > macOS Generic Icon.
-
I named that DocIcon and filled in all the sizes.
-
I edited the Info.plist file to look like that shown at the end of this post. There’s a bunch of important changes here:
-
I switched from UTImportedTypeDeclarations to UTExportedTypeDeclarations. That’s necessary because I want to define a new file type. The document app template imports public.plain-text and uses that as its file type.
-
I set the content type to be a unique value, com.example.apple-samplecode.Test783646.doc.
-
I conformed that to public.content and public.data, for reasons I explained above.
-
I added .test783646doc as its extension.
-
And DocIcon as its icon.
-
And set the description.
-
In Test783646Document.swift, I change the exampleText property to be my document type, that is, com.example.apple-samplecode.Test783646.doc.
-
In the target editor, I changed the deployment target to macOS 15.0.
-
I built the app.
The app structure now looks like this:
% find Test783646.app
Test783646.app
Test783646.app/Contents
Test783646.app/Contents/_CodeSignature
Test783646.app/Contents/_CodeSignature/CodeResources
Test783646.app/Contents/MacOS
Test783646.app/Contents/MacOS/Test783646
Test783646.app/Contents/MacOS/__preview.dylib
Test783646.app/Contents/MacOS/Test783646.debug.dylib
Test783646.app/Contents/Resources
Test783646.app/Contents/Resources/AppIcon.icns
Test783646.app/Contents/Resources/Assets.car
Test783646.app/Contents/Info.plist
Test783646.app/Contents/PkgInfo
Xcode has unpacked AppIcon because that’s a strict requirement. It’s left DocIcon in the asset catalogue because that placement is compatible with my deployment target.
I then tested the app:
-
I restored a macOS 15.3.2 VM to a clean snapshot. Note that there’s nothing special about that version number; it’s just what I have lying around.
-
I copied the app to the VM using scp.
-
In the VM, I see that the app has the right icon.
-
I launched the app and saved a new document to disk. It has the right document icon.
Now, I realise that you don’t want to use an asset catalogue, so I rebuilt the app to use a .icns:
-
I extract a .iconset from the asset catalogue of the built app:
% iconutil -c iconset -o DocIcon.iconset Test783646.app/Contents/Resources/Assets.car DocIcon
I could’ve built this .iconset manually, but extracting it was easier (-:
-
In Xcode, I removed DocIcon from the asset catalogue.
-
And added the DocIcon.iconset that I created in step 1.
-
I built the app.
Now the built app looks like this:
% find Test783646.app
Test783646.app
…
Test783646.app/Contents/Resources/DocIcon.icns
Test783646.app/Contents/Resources/AppIcon.icns
…
And there’s no sign of DocIcon in the asset catalogue:
% assetutil -I Test783646.app/Contents/Resources/Assets.car | grep DocIcon
%
I then repeated my test process and confirmed that the doc icon still works.
IMPORTANT Note how the test process involves restoring my VM from a fresh snapshot, so I’m 100% sure that my first test run won’t interfere with the second.
So, in summary, in the built app, I have this:
% find Test783646.app
Test783646.app
…
Test783646.app/Contents/Resources/DocIcon.icns
Test783646.app/Contents/Resources/AppIcon.icns
…
% file Test783646.app/Contents/Resources/DocIcon.icns
Test783646.app/Contents/Resources/DocIcon.icns: Mac OS X icon, 37195 bytes, "ic12" type
and this:
% plutil -p Test783646.app/Contents/Info.plist
{
…
"CFBundleDocumentTypes" => [
0 => {
"CFBundleTypeRole" => "Editor"
"LSHandlerRank" => "Default"
"LSItemContentTypes" => [
0 => "com.example.apple-samplecode.Test783646.doc"
]
"NSUbiquitousDocumentUserActivityType" => "com.example.apple-samplecode.Test783646.exampledocument"
}
]
…
"UTExportedTypeDeclarations" => [
0 => {
"UTTypeConformsTo" => [
0 => "public.content"
1 => "public.data"
]
"UTTypeDescription" => "Test783646 document"
"UTTypeIconFile" => "DocIcon"
"UTTypeIdentifier" => "com.example.apple-samplecode.Test783646.doc"
"UTTypeTagSpecification" => {
"public.filename-extension" => [
0 => "test783646doc"
]
}
}
]
}
I think this should be enough to get you on track, but let me know otherwise.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>com.example.apple-samplecode.Test783646.doc</string>
</array>
<key>NSUbiquitousDocumentUserActivityType</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).exampledocument</string>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.content</string>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Test783646 document</string>
<key>UTTypeIconFile</key>
<string>DocIcon</string>
<key>UTTypeIdentifier</key>
<string>com.example.apple-samplecode.Test783646.doc</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>test783646doc</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>