3

I'm trying to create a function that pauses AR session and also clears my dictionary variable entityDictionaries and removes all anchors. However, I'm kinda stuck and don't know how to start.

Here is a code for my main view:

    struct InitiateARView: View {
    @StateObject var vm = ARPreparationViewModel()
     
    var body: some View {
        VStack {
             if vm.isLoading {
                 Button("Start", action: {
                     vm.prepareAR()
                 })
                 
             } else {
                 ZStack {
                     ARExperience()
                         .edgesIgnoringSafeArea(.all)
                     VStack {
                         HStack {
                             Spacer()
                             Button("Stop AR", action: {
                                // Call to clear AR session function here
                             })
                         }
                         Spacer()
                     }
                 }
             }  
         }
     }
}

And here is simplified version of my ARExperience struct

struct ARExperience: UIViewRepresentable {
    var arView = ARView(frame: .zero) 
    
    var entityDictionaries = [String: (anchor: AnchorEntity, model: ModelEntity)]()
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    class Coordinator: NSObject, ARSessionDelegate {
        var parent: ARExperience
    
        init(parent: ARExperience) {
            self.parent = parent
        }
        
        func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
            guard let validAnchor = anchors[0] as? ARImageAnchor else { return }
            let anchor = AnchorEntity(anchor: validAnchor)
            let videoPlane = createdVideoPlayerNodeFor(validAnchor.referenceImage)
            anchor.addChild(videoPlane)
            parent.arView.scene.addAnchor(anchor)
            if let targetName = validAnchor.referenceImage.name {
                parent.entityDictionaries[targetName] = (anchor, videoPlane)
            }
        }

        func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {

        }
        
    }

    func makeUIView(context: Context) -> ARView {
        //...        
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {

    }
}

Any help that can get me 'unstuck' with this problem will be perfect

user1207524
  • 251
  • 2
  • 12
  • 27
  • Any of these may be relevant in terms of how to communicate from SwiftUI to the represented view: https://stackoverflow.com/questions/70958776/perform-scnaction-using-a-button-swiftui https://stackoverflow.com/questions/69442079/pause-a-spritekit-scene-from-swiftui-using-spriteviewispaused-without-reinit https://stackoverflow.com/questions/66619751/complex-uikit-swiftui-interface-via-uiviewcontrollerrepresentable – jnpdx Jun 03 '23 at 14:38
  • You've got some basic mistakes, StateObject isn't designed for view model objects you need to learn the View struct. And then you'll realise Coordinator(parent: self) is a bug because self is a value not an object. – malhal Jun 04 '23 at 13:52

1 Answers1

1

One way to do it is to separate UIKit from SwiftUI and have a Common source of truth where the 2 frameworks can communicate with each other.

struct InitiateARView: View {
    @StateObject var vm = ARPreparationViewModel()
    
    var body: some View {
        VStack {
            if vm.isLoading {
                Button("Start", action: {
                    vm.prepareAR()
                })
                
            } else {
                ZStack {
                    ARExperience_UI(vm: vm) //Your experiece would change to include the shared VM
                        .edgesIgnoringSafeArea(.all)
                    VStack {
                        HStack {
                            Spacer()
                            Button("Stop AR", action: {
                                //Make SwiftUI changes
                                vm.clearAR() // Then you can access UIKit via the VM
                            })
                        }
                        Spacer()
                    }
                }
            }
        }
    }
}

The shared source of truth would look something like.

//Works with both SwiftUI and UIKit
class ARPreparationViewModel: ObservableObject{
    //reference to UIKit, will be nil until set by ViewController init.
    @Published var vc: ARExperienceVC?
    @Published var isLoading: Bool = false
    
    deinit {
        vc = nil
    }
    
    func prepareAR() {
        guard let vc = vc else {
            print("Missing View Controller")
            return
        }
        vc.prepareAR()
    }
    func clearAR() {
        guard let vc = vc else {
            print("Missing View Controller")
            return
        }
        //Make any source of truth changes
        vc.clearAR()
    }
}

Then the UIViewControllerRepresentable code would be simplified to just create the new UIViewController, this just serves as a bridge.

// SwiftUI
struct ARExperience_UI: UIViewControllerRepresentable { //Change to a UIViewControllerRepresentable
    let vm: ARPreparationViewModel
    
    func makeUIViewController(context: Context) -> some UIViewController {
        ARExperienceVC(vm: vm) // Create the new ViewController with all the UIKit Code
    }
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        
    }
}

The UIKit code does the heavy lifting.

// Keep UIKit code here
class ARExperienceVC: UIViewController {
    let vm: ARPreparationViewModel

    lazy var arView: ARView = {
        let arView = ARView(frame: .zero)
        arView.session.delegate = self
        
        return arView
    }()
    
    init(vm: ARPreparationViewModel) {
        self.vm = vm
        super.init(nibName: nil, bundle: nil)
        self.vm.vc = self //Without this the source of truth won't have access to the ViewController
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        self.vm.vc = nil
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(arView)
        arView.pinToSuperview(self.view) // This lest SwiftUI control the size because the view will be attached to the bordering anchors.
    }
    
    func prepareAR() {
        print("\(type(of: self)) :: \(#function)")
    }
    func clearAR() {
        print("\(type(of: self)) :: \(#function)")
        //Make UIKit changes.
    }
}

extension ARExperienceVC: ARSessionDelegate {
    func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
        
    }

    func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {

    }
}

extension UIView{
    func pinToSuperview(_ superView: UIView){
        self.translatesAutoresizingMaskIntoConstraints = false
        self.topAnchor.constraint(equalTo: superView.topAnchor).isActive = true
        self.bottomAnchor.constraint(equalTo: superView.bottomAnchor).isActive = true
        self.leadingAnchor.constraint(equalTo: superView.leadingAnchor).isActive = true
        self.trailingAnchor.constraint(equalTo: superView.trailingAnchor).isActive = true
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48