Create custom document type with FileWrapper

Viewed 717

I want to create a SwiftUI macOS app that creates its own data type. It should create files with the extension .frogmd, which is supposed to be a package containing

  • a json file representing the document
  • a images folder containing images

Right now, I am able to save my content to disk in a folder containing with above mentioned structure, but it does not create a file with my desired extension, representing a package with the content.

My educated guess is that I am doing something wrong in the Exported Type Identifiers section in my Info.plist, so here's that part of my plist:

<key>UTExportedTypeDeclarations</key>
    <array>
        <dict>
            <key>UTTypeConformsTo</key>
            <array>
                <string>public.data</string>
            </array>
            <key>UTTypeDescription</key>
            <string>Frog's MD Document</string>
            <key>UTTypeIcons</key>
            <dict/>
            <key>UTTypeIdentifier</key>
            <string>ch.appfros.frogmd</string>
            <key>UTTypeTagSpecification</key>
            <dict>
                <key>public.filename-extension</key>
                <array>
                    <string>frogmd</string>
                </array>
            </dict>
        </dict>
    </array>

In case it should help, here's the application's code so far:

@main
struct poc_Markdown2App: App {
    var body: some Scene {
        DocumentGroup(newDocument: FrogMdDocumentWrapper()) { file in
            ContentView(document: file.$document.document)
        }
    }
}

struct FrogMdDocument: Codable {
    var id: UUID
    var title: String
    var textContent: String
    var imageUrls: [URL]
    
    init(id: UUID = .init(), title: String = "New Document", textContent: String = .init(), imageUrls: [URL] = .init()) {
        self.id = id
        self.title = title
        self.textContent = textContent
        self.imageUrls = imageUrls
    }
}

struct FrogMdDocumentWrapper: FileDocument {
    var document: FrogMdDocument
    
    static var readableContentTypes = [UTType.data]
    
    init(document: FrogMdDocument = FrogMdDocument()) {
        self.document = document
    }
    
    init(configuration: ReadConfiguration) throws {
        self.init()
    }
    
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        do {
            let data = try getDocumentData()
            let jsonFileWrapper = FileWrapper(regularFileWithContents: data)
            let filename = "document.json"
            jsonFileWrapper.filename = filename
            
            //TODO: store images to this imagesFileWrapper
            let imagesFileWrapper = FileWrapper(directoryWithFileWrappers: [String : FileWrapper]())
            
            let fileWrapper = FileWrapper(directoryWithFileWrappers: [
                filename: jsonFileWrapper,
                "images": imagesFileWrapper
            ])
            
            return fileWrapper
        } catch {
            throw error
        }
    }

    private func getDocumentData() throws -> Data {
        let encoder = JSONEncoder()
        do {
            let data = try encoder.encode(document)
            return data
        } catch {
            throw error
        }
    }
    
}
2 Answers

I found it (and will post here for future reference :-) ):

I was missing a plist entry in my Document Types entry. Under Additional document type properties, make sure that LSTypeIsPackage is set to YES.

document type setup including LSTypeIsPackage

As I was not able to add mentioned key to in the Info interface directly, I did add it in the Info.plist itself, resulting in above setup; see the corresponding Info.plist portion below:

<key>CFBundleDocumentTypes</key>
    <array>
        <dict>
            <key>LSTypeIsPackage</key>
            <true/>
            <key>CFBundleTypeIconSystemGenerated</key>
            <integer>0</integer>
            <key>CFBundleTypeName</key>
            <string>Frog Markdown</string>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>LSHandlerRank</key>
            <string>Default</string>
            <key>LSItemContentTypes</key>
            <array>
                <string>ch.appfros.frogmd</string>
            </array>
            <key>NSUbiquitousDocumentUserActivityType</key>
            <string>$(PRODUCT_BUNDLE_IDENTIFIER).frogmd</string>
        </dict>
    </array>

Thank you for this. I was having the exact same problem. I followed your guidance on how to save multiple child files into a document package but was still ending up with all of them saved into a folder. I finally traced my problem down to the declaration of my exported UTType as conforming to "public.data", which is in line with the guidance given by Apple on Defining File and Data Types for Your App, but still failed to work. I found that if I made my exported UTType conform to "com.apple.package" instead of "public.data", my filewrapper was written to a document package as intended.

You may wish to consider adding this bit of code

extension UTType {
    static let frogmd = UTType(exportedAs: "ch.appfros.frogmd")
}

before the declaration of your FrogMdDocument struct and replacing

static var readableContentTypes = [UTType.data]

with

static var readableContentTypes = [UTType.frogmd]

Filewrappers are then saved as document packages with the "frogmd" extension automatically and double clicking on a document package with the "frogmd" extension in Finder loads the package into its parent application by default. I am running Xcode 12.5 under macOS 11.4 and suspect that your solution probably works just fine with earlier versions of Xcode and macOS? However and in any event, your post steered me in the right direction so thanks again. I also found that the info.plist "LSTypeIsPackage" has been replaced by "Document is a package or bundle"; however, "LSTypeIsPackage" appears still to work.

Related