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)
}