SCNScene.write(to:) for usdz export only works for first call

I am using scene.write(to:"dirpath\name.usdz") to get usdz export functionality into my app (universal, macOS & iOS). My problem is, it ceases to work after the first use, quitting & restarting the app is the only way to re-enable it. I have tried reusing the same scene, and instantiating a new scene (both ways with the exact same node structure), same results every time: first invocation writes a file of ~14MB, any calls after that write 1.5-2k of garbage. I use a unique filename for each write, and check to make sure it doesn't already exist.

Any ideas?

The behavior is caused by two separate issues with a SwiftUI Sandboxed app:

First, the SCNScene needs to be rendered in order to export correctly. You can't do what I was doing (create a bunch of nodes, stuff them into the scene's root node and call scene.write) and get a correctly rendered usdz. It must first be put on screen in a SwiftUI SceneView, which causes a lot of other initialization to occur. I suppose you could instantiate a SCNRenderer and call prepare() on the root node, but that has some extra complications.

Second, the Sandbox prevents a direct export to a URL provided by .fileExporter(). This is because Scene.write() works in two steps: it first creates a .usdc export, and zips the resulting files into a single .usdz. The intermediate files don't have the write privileges the URL provided by .fileExporter() does (assuming you've set the Sandbox "User Selected File" privilege to "Read/Write"), so Scene.write() fails, even if the target URL is writeable, if the target directory is outside the Sandbox.

My solution was to write a custom FileWrapper, which I return if the WriteConfiguration UTType is .usdz:

public class USDZExportFileWrapper: FileWrapper {
    var exportScene: SCNScene

    public init(scene: SCNScene) {
        exportScene = scene
        super.init(regularFileWithContents: Data())
    }

    required init?(coder inCoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override public func write(to url: URL,
                               options: FileWrapper.WritingOptions = [],
                               originalContentsURL: URL?) throws {
        let tempFilePath = NSTemporaryDirectory() + UUID().uuidString + ".usdz"
        let tempURL = URL(fileURLWithPath: tempFilePath)
        exportScene.write(to: tempURL, delegate: nil)
        try FileManager.default.moveItem(at: tempURL, to: url)
    }
}

Usage in a ReferenceFileDocument:

public func fileWrapper(snapshot: Data, configuration: WriteConfiguration) throws -> FileWrapper {
    if configuration.contentType == .usdz {
        return USDZExportFileWrapper(scene: scene)
    }

    return .init(regularFileWithContents: snapshot)
}
SCNScene.write(to:) for usdz export only works for first call
 
 
Q