horseshoe7
horseshoe7

Reputation: 2827

How to I wrap a UIKit view in SwiftUI with a delegate callback requiring a return value?

The concrete usecase is that I want to wrap a ARSCNView in UIViewRepresentable.

The thing is, I want to expose the ARSCNView's following properties: .delegate .session.delegate .session.delegateQueue

and consider that some of the ARSCNViewDelegate method signatures have return values:

func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode?

and I'm using this view in a pattern that has a

@StateObject var viewModel: ViewModel // where ViewModel runs on @MainActor

Now, I do have an approach that sort of works, but it doesn't handle the concurrency well, since any message sent to the viewModel would occur on the MainActor, and then the .session.delegateQueue would essentially be useless, no?

This is what I have so far, but to me, it "smells funny":

struct ARSCNSwiftUIView: UIViewRepresentable {
    
    let sessionConfiguration: ARConfiguration
    @Binding var isRunning: Bool
    
    let onRendererNeedsNodeForAnchor: (SCNSceneRenderer, ARAnchor) -> SCNNode?
    let onRendererDidUpdateNodeForAnchor: (SCNSceneRenderer, SCNNode, ARAnchor) -> Void
    let onARSessionDidUpdateFrame: (ARSession, ARFrame) -> Void
    
    func makeUIView(context: Context) -> ARSCNView {
        
        let view = ARSCNView()
        
        guard ARWorldTrackingConfiguration.isSupported else { return view }
        
        view.delegate = context.coordinator
        view.session.delegate = context.coordinator
        
        // TODO: view.session.delegateQueue = coordinator.workerQueue then a view model has a nonisolated method to handle these?
        #if DEBUG
        view.showsStatistics = true
        #endif
        return view
    }
    
    func updateUIView(_ view: ARSCNView, context: Context) {
        if self.isRunning {
            view.session.run(self.sessionConfiguration, options: [.resetTracking, .removeExistingAnchors])
        } else {
            view.session.pause()
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, ARSCNViewDelegate, ARSessionDelegate {
        var parent: ARSCNSwiftUIView
        init(_ parent: ARSCNSwiftUIView) {
            self.parent = parent
        }
        
        func session(_ session: ARSession, didUpdate frame: ARFrame) {
            self.parent.onARSessionDidUpdateFrame(session, frame)
        }
        
        public func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
            return self.parent.onRendererNeedsNodeForAnchor(renderer, anchor)
        }
        
        public func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
            self.parent.onRendererDidUpdateNodeForAnchor(renderer, node, anchor)
        }
    }
}

Upvotes: 0

Views: 175

Answers (1)

malhal
malhal

Reputation: 30746

you cant use self because it's a struct value not a object reference so change to:

 func makeCoordinator() -> Coordinator {
        Coordinator()
    }

Also you need to use the new version of the closures, e.g.

func updateUIView(_ view: ARSCNView, context: Context) {
    context.coordinator.onARSessionDidUpdateFrame = onARSessionDidUpdateFrame
    // etc.
    class Coordinator: NSObject, ARSCNViewDelegate, ARSessionDelegate {
         var onARSessionDidUpdateFrame: ((ARSession, ARFrame) -> Void)?
    
        func session(_ session: ARSession, didUpdate frame: ARFrame) {
            onARSessionDidUpdateFrame(session, frame)
        }

Upvotes: 0

Related Questions