swift – iOS: NavigationStack.navigationDestination breaks ARKit Session used in RealityKit ARView


The following code is described as doing (this is related to Reality Kit ARView, although I am accessing the underlying ARKit Session as you will see):

  • App opens to a .nonAR ARView FirstARView
  • This will show a red background with a blue box showing a single side of the box face on
  • There will be some red text which says Press the button, when you press this the app uses a NavigationStack.navigationDestination to open a second RealityKit ARView MagicARView

Before the MagicARView opens the following occurs to the ARKit session from .nonAR ARView FirstARView

  1. Pause
  2. Change camera mode to .AR
  3. Change the background to be .CameraFeed()
  4. Run()
  5. Pause()

Note on why you have to do the above steps 1 to 5:
I had a previous issue when switching between a view with a .nonAR view and an .AR view where the camera feed on the .AR view was just black – the only way to overcome this seems to be running the above steps on the underlying ARKit session before switching to the second .AR view.

Now when the second .AR MagicARView opens there will be a large blue box which appears at [0,0,0].

Expected behaviour: you can move around the room (eg: move behind a partial wall) and the box will be occluded by the wall.

Actual behaviour: you can move around the room (eg: move behind a partial wall) and the box IS NOT occluded by the wall – occlusion is broken.

My assumption: Even through you see the .nonAR ARView FirstARView disappearing, and even through I go to great lengths to set both the arView in the FirstController and even the FirstController itself to nil – something is being held onto incorrectly in the underlying ARKit Session (apparently there is only one per app…)

My solution and question: See the second code block for the solution – if you remove the .navigationDestination and use conditionals in the ContentView it appears to resolve the issue. Does anyone understand the inc.navigationDestination and ARKit session and why this is the case? Note you obviously do not even need to pause() and change the camera when doing it this way.

import SwiftUI
import RealityKit
import ARKit

let configuration: ARWorldTrackingConfiguration = {
    let config = ARWorldTrackingConfiguration()
    
    if (ARWorldTrackingConfiguration.supportsSceneReconstruction(ARWorldTrackingConfiguration.SceneReconstruction.mesh)) {
        config.sceneReconstruction = .mesh
    }
    
    config.planeDetection = [.vertical, .horizontal]
    
    return config
}()

class FirstViewController {
    weak var arView: ARView?
}

class FirstViewControllerWrapper {
    var firstController: FirstViewController?
    
    init() {
        firstController = FirstViewController()
    }
}

struct ContentView: View {
    @State var loadSecondView: Bool = false
    
    var firstViewControllerWrapper: FirstViewControllerWrapper?
    
    init () {
        firstViewControllerWrapper = FirstViewControllerWrapper()
    }
    
    var body: some View {
        NavigationStack{
            ZStack{
                FirstARView(firstViewController: firstViewControllerWrapper!.firstController!)
                    .onDisappear(){
                        print("First view is disappearing")
                    }
                Button(action: {
                    firstViewControllerWrapper?.firstController!.arView?.session.pause()
                    firstViewControllerWrapper?.firstController!.arView?.cameraMode = .ar
                    firstViewControllerWrapper?.firstController!.arView?.environment.background = .cameraFeed()
                    firstViewControllerWrapper?.firstController!.arView?.session.run(configuration)
                    firstViewControllerWrapper?.firstController!.arView?.session.pause()
                    firstViewControllerWrapper?.firstController!.arView = nil
                    firstViewControllerWrapper?.firstController = nil
                    loadSecondView = true
                    
                }) {
                    Text("Press the button").background(.red)
                }
            }
            .navigationDestination(isPresented: $loadSecondView) {
                MagicARView()
            }
        }
    }
}

struct MagicARView: UIViewRepresentable {

    func makeUIView(context: Context) -> ARView {
        
        let arView = ARView(frame: .zero, cameraMode: .ar, automaticallyConfigureSession: true)
        arView.environment.sceneUnderstanding.options.insert(.occlusion)

        let boxMesh = MeshResource.generateBox(size: 0.5)
        let boxMaterial = SimpleMaterial(color: .blue, isMetallic: false)
        let model = ModelEntity(mesh: boxMesh, materials: [boxMaterial])
        let modelAnchor = AnchorEntity(world: [0.2,0.2,0.2])
        modelAnchor.addChild(model)
        arView.scene.addAnchor(modelAnchor)
        arView.session.run(configuration)
        return arView
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {
        
    }
    
}

struct FirstARView: UIViewRepresentable {
    
    weak var firstViewController: FirstViewController?
    
    func makeUIView(context: Context) -> ARView {
    
        let arView = ARView(frame: .zero, cameraMode: .nonAR, automaticallyConfigureSession: true)
        arView.environment.sceneUnderstanding.options.insert(.occlusion)

        let boxMesh = MeshResource.generateBox(size: 0.5)
        let boxMaterial = SimpleMaterial(color: .blue, isMetallic: false)
        let model = ModelEntity(mesh: boxMesh, materials: [boxMaterial])
        let modelAnchor = AnchorEntity(world: [0.2,0.2,0.2])
        modelAnchor.addChild(model)
        arView.scene.addAnchor(modelAnchor)
        arView.session.run(configuration)
        arView.environment.background = .color(.red)
        firstViewController!.arView = arView
        
        return arView
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {
        
    }
    
}

Solution?

import SwiftUI
import RealityKit
import ARKit

let configuration: ARWorldTrackingConfiguration = {
    let config = ARWorldTrackingConfiguration()
    
    if (ARWorldTrackingConfiguration.supportsSceneReconstruction(ARWorldTrackingConfiguration.SceneReconstruction.mesh)) {
        config.sceneReconstruction = .mesh
    }
    
    config.planeDetection = [.vertical, .horizontal]
    
    return config
}()

class FirstViewController {
    weak var arView: ARView?
}

class FirstViewControllerWrapper {
    var firstController: FirstViewController?
    
    init() {
        firstController = FirstViewController()
    }
}

struct ContentView: View {
    @State var loadSecondView: Bool = false
    
    var firstViewControllerWrapper: FirstViewControllerWrapper?
    
    init () {
        firstViewControllerWrapper = FirstViewControllerWrapper()
    }
    
    var body: some View {
        NavigationStack{
            ZStack{
                if (!loadSecondView) {
                    FirstARView(firstViewController: firstViewControllerWrapper!.firstController!)
                        .onDisappear(){
                            print("First view is disappearing")
                        }
                }
                if (loadSecondView){
                    MagicARView()
                }
                Button(action: {
         
                    loadSecondView = true
                    
                }) {
                    Text("Press the button").background(.red)
                }
            }
            
        }
    }
}

struct MagicARView: UIViewRepresentable {

    func makeUIView(context: Context) -> ARView {
        
        let arView = ARView(frame: .zero, cameraMode: .ar, automaticallyConfigureSession: true)
        arView.environment.sceneUnderstanding.options.insert(.occlusion)

        let boxMesh = MeshResource.generateBox(size: 0.5)
        let boxMaterial = SimpleMaterial(color: .blue, isMetallic: false)
        let model = ModelEntity(mesh: boxMesh, materials: [boxMaterial])
        let modelAnchor = AnchorEntity(world: [0.2,0.2,0.2])
        modelAnchor.addChild(model)
        arView.scene.addAnchor(modelAnchor)
        arView.session.run(configuration)
        return arView
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {
        
    }
    
}

struct FirstARView: UIViewRepresentable {
    
    weak var firstViewController: FirstViewController?
    
    func makeUIView(context: Context) -> ARView {
    
        let arView = ARView(frame: .zero, cameraMode: .nonAR, automaticallyConfigureSession: true)
        arView.environment.sceneUnderstanding.options.insert(.occlusion)

        let boxMesh = MeshResource.generateBox(size: 0.5)
        let boxMaterial = SimpleMaterial(color: .blue, isMetallic: false)
        let model = ModelEntity(mesh: boxMesh, materials: [boxMaterial])
        let modelAnchor = AnchorEntity(world: [0.2,0.2,0.2])
        modelAnchor.addChild(model)
        arView.scene.addAnchor(modelAnchor)
        arView.session.run(configuration)
        arView.environment.background = .color(.red)
        firstViewController!.arView = arView
        
        return arView
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {
        
    }
    
}

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img