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)
}
Post
Replies
Boosts
Views
Activity
And yes, I did try @MainActor on the document class...It causes a whole slew of new warnings, and doesn't fix the data race.
I just got this to work in my app by making the selection variable non-optional, e.g.
@State var selection: NavigationItem = .view1
Now the sidebar list shows up with the correct item highlighted. The initial state provided in the declaration can be overridden in the onAppear view modifier, i.e.
.onAppear { selection = .view2 }
Not sure this works if a nil selection state is a required option. Perhaps provide a .none enum case, and don't embed it in the list?
For me too - crashed with SIGABRT on launch, console output:
dyld[3234]: Symbol not found: _$s19_RealityKit_SwiftUI20ObjectCaptureSessionC7Combine010ObservableE0AAMc
Referenced from: <294EDA13-E14C-3BFF-B705-8368778FF69F> /private/var/containers/Bundle/Application/255DCE73-DCDD-489F-8A6F-83DA8E04026C/GuidedCapture.app/GuidedCapture
Expected in: <6A96F77C-1BEB-3925-B370-266184BF844F> /System/Library/Frameworks/_RealityKit_SwiftUI.framework/_RealityKit_SwiftUI
This just started happening to us in iOS 18. I noticed that HEIC files produced by the Object Capture API didn't have this problem, and it turns out the AVDepthData returned with AVCapturePhoto is in a disparity format, and needs to be converted to a depth format before retrieving it as a dictionary. I created an extension that handles this:
import Foundation
extension OSType {
fileprivate func fourCCToString() -> String {
let utf16 = [
UInt16((self >> 24) & 0xFF),
UInt16((self >> 16) & 0xFF),
UInt16((self >> 8) & 0xFF),
UInt16((self & 0xFF)),
]
return String(utf16CodeUnits: utf16, count: 4)
}
}
extension AVDepthData {
public func formattedForPhotogrammetry() -> AVDepthData {
if depthDataType == kCVPixelFormatType_DepthFloat32 {
return self
} else if canCovertToDepthFloat32() {
print(
"converting \(depthDataType.fourCCToString()) to \(kCVPixelFormatType_DepthFloat32.fourCCToString())"
)
return self.converting(
toDepthDataType: kCVPixelFormatType_DepthFloat32)
} else {
return self
}
}
public func canCovertToDepthFloat32() -> Bool {
return availableDepthDataTypes.contains(kCVPixelFormatType_DepthFloat32)
}
}
Now you can add this to an image destination, and get the Data() to write to a file:
import CoreImage
import Foundation
extension CGImageDestination {
func addImage(from imageSource: CGImageSource) {
CGImageDestinationAddImageFromSource(self, imageSource, 0, nil)
}
func addDepthData(from depthData: AVDepthData) {
guard
var depthDictionary = depthData.formattedForPhotogrammetry()
.dictionaryRepresentation(
forAuxiliaryDataType: nil)
else { return }
// looking at images from ObjectCaptureSession, depth metadata isn't supplied,
// so no sense in including it ourselves
depthDictionary.removeValue(forKey: kCGImageAuxiliaryDataInfoMetadata)
CGImageDestinationAddAuxiliaryDataInfo(
self, kCGImageAuxiliaryDataTypeDepth,
depthDictionary as CFDictionary)
}
func finalize() -> Bool {
CGImageDestinationFinalize(self)
}
}
Hope this helps.
After some further communication with DTS (Thanks Paris!), a workaround is to use XXViewRepresentable to wrap a platform-specific progress view. Here is my solution:
import SwiftUI
#if os(macOS)
import AppKit
public typealias SuperClass = NSViewRepresentable
#else
import UIKit
public typealias SuperClass = UIViewRepresentable
#endif
public struct RepresentedProgressView: SuperClass {
@State public private(set) var progress: Progress
public init(_ progress: Progress) {
self.progress = progress
}
#if os(macOS)
public func makeNSView(context: Self.Context) -> NSProgressIndicator {
let view = NSProgressIndicator()
view.observedProgress = progress
return view
}
public func updateNSView(_ uiView: Self.NSViewType, context: Self.Context) {
}
#else
public func makeUIView(context: Self.Context) -> UIProgressView {
let view = UIProgressView()
view.observedProgress = progress
return view
}
public func updateUIView(_ uiView: Self.UIViewType, context: Self.Context) {
}
#endif
}
This is working in our code, and doesn't crash with > 60 files transferred