Provide EnvironmentObject TrainerView

TODO: collapasable, copyable, download, etc

In my swift ui app, I have this app file ```// // LaserSwordTrainerApp.swift // LaserSwordTrainer // // Created by Austin King on 5/24/24. //

import SwiftUI

@main struct LaserSwordTrainerApp: App {

@Environment(\.scenePhase) var scenePhase
    
@State var currentImmersionStyle: ImmersionStyle = .mixed
@StateObject var multipeerManager =  MultipeerManager()

init() {
    
    print("LaserSwordTrainerApp We've got a multipeerManager \(multipeerManager)")
    
    SyncIPhoneComponent.registerComponent()
    // Components in the RealityKitContent are automatically registered?
    // SaberComponent.registerComponent()
    SyncIPhoneSystem.registerSystem()
    TrainingTargetSystem.registerSystem()
}

var body: some Scene {
    WindowGroup {
        RoutingView()
    }.defaultSize(CGSize(width: 250 * 2.0, height: 800 ))
    .environmentObject(multipeerManager)
    .onChange(of: scenePhase) { oldScene, newScene in
        switch newScene {
        case .active:
            print("App became active")
            multipeerManager.start()
        case .background:
            print("App became background")
            multipeerManager.stop()
        case .inactive:
            print("App became inactive")
        default:
            print("Unknown scenePhase \(newScene)")
        }
        
    }
    ImmersiveSpace(id: "TrainerSpace") {
        TrainerView().environmentObject(self.multipeerManager)
    }
        .immersionStyle(selection: $currentImmersionStyle, in: .mixed, .progressive, .full)
        .upperLimbVisibility(.hidden)
    
}

}

and this immersive View ```//
//  StackingView.swift
//  StackingSpaceTutorial
//
//  Created by Austin King on 5/27/24.
//

import SwiftUI
import RealityKit
import RealityKitContent

struct TrainerView: View {
    
    @EnvironmentObject var multipeerManager: MultipeerManager
    
    
    
    var soundManager = SoundManager()
    
    // TODO: do these go here or somewhere else?
    @StateObject var model = HandTrackingViewModel.singleton
    
    // TODO: do we need @State here?
    @StateObject var laserSwordModel = LaserSwordViewModel()
    
    var enemy = Enemy()
    
    @State var priorCollisions: [UInt64: Bool] = [:]
    
    init() {
        print("TrainerView We've got a multipeerManager \(multipeerManager)")
         
        
        self.multipeerManager.setLaserSwordViewModel(laserSwordModel: self.laserSwordModel)
        
        
        SyncIPhoneSystem.multipeerManager = self.multipeerManager
        SyncIPhoneSystem.handTrackingViewModel = self.model
        
    }
    
    var body: some View {
        RealityView {
            content in
            print("TrainerView RealityView running")
            
            // model.setSoundManager(soundManager: soundManager)
            
            // TODO: multipeerManager.setSoundManager(soundManager: soundManager)
            
            content.add(await model.setupContentEntity(aSoundManager: soundManager))
            self.multipeerManager.setSoundManager(soundManager:soundManager)
            
            content.add(await enemy.createEnemy())
            
            subscribeToCollisions(content:content)
            
            await addPreviewSaber(content: content)
            
            
            
        } update: { content in
            print("update called isLaserSwordOn= \(self.laserSwordModel.isLaserSwordOn)")
            // let entity = content.entities.first!
            // TODO: find and pause the particle systm
            // TODO: find and shrink the cylinder
            // content.entities
            if let cylinder = content.entities.first?.findEntity(named: "Cylinder") {
                
                print("Found cylinder! \(cylinder.id)")
                // TODO: AOK cylinder.name = "lightsaber"
                
                
                // Access the ParticleEmitter component and toggle isEmitting
                if var particleEmitterEntity = cylinder.findEntity(named: "ParticleEmitter") {
                    // if var particleEmitter = cylinder.components[ParticleEmitterComponent.self] {
                    print("Found particle emitter")
                    
                    // Ensure the entity has a ParticleEmitterComponent
                    if var particleEmitterComponent = particleEmitterEntity.components[ParticleEmitterComponent.self] {
                        print("Foudn the ParticleEmitter component")
                        // Toggle the isEmitting property
                        particleEmitterComponent.isEmitting = self.laserSwordModel.isLaserSwordOn
                        particleEmitterEntity.components[ParticleEmitterComponent.self] = particleEmitterComponent
                    } else {
                        print("ParticleEmitterComponent not found on the entity.")
                    }
                    
                    
                } else {
                    print("No particle emitter found")
                }
                
                // Change the scale.y to 1.0
                
                Task {
                    try? await Task.sleep(for: .milliseconds(100))
                    cylinder.scale.y = laserSwordModel.isLaserSwordOn ? 3.0 : 0.0
                    cylinder.isEnabled = laserSwordModel.isLaserSwordOn
                }
                
                // position should go from -5.0 to to 0?
            } else {
                print("Entity named 'Cylinder' not found.")
            }
        }.task {
            // Anything that depends on lightsaber having been loaded should go here
            print("Initializing finger tips")
            // await model.initFingertips()
            print("setting up content entities")
            
            // run ARKitSession
            print("Running session")
            await model.runSession()
            // process hand updates
            print("Processing hand updates")
            await model.processHandUpdates()
        }.task {
            // process our world reconstruction
            
        }.gesture(SpatialTapGesture().targetedToAnyEntity().onEnded({
            value in
            Task {
                // Place our cube
            }
            
        }))
        /* .onAppear() {
            print("ContentView appearing")
            self.multipeerManager.start()
            
        }.onDisappear(perform: {
            print("ContentView disappearing")
            self.multipeerManager.stop()
        })*/
    }
    
    
    
    func subscribeToCollisions(content: RealityViewContent) {
        // TODO: do we need to cache the entity id to avoid spawning multiple times?
        let sub = content.subscribe(to: CollisionEvents.Began.self) {event in
            if event.entityA.name == "Enemy" && nil == (priorCollisions[event.entityA.id] ?? nil) {
                handleCollision(enemyEntity: event.entityA, lightSaber: event.entityB, content: content)
            } else if event.entityB.name == "Enemy" && nil == (priorCollisions[event.entityB.id] ?? nil) {
                handleCollision(enemyEntity: event.entityB, lightSaber: event.entityA, content: content)
            } else {
                print("Skipping a collision we've seen already \(event.entityA.id) or \(event.entityB.id)")
            }
            
            print("We have a collision folks entityA= \(event.entityA.name)")
            print("We have a collision folks entityB= \(event.entityB.name)")
            // TODO: play particles
            // TODO: play explosion
        }
        // TODO: sub.unsub
    }
    
    func handleCollision(enemyEntity: Entity, lightSaber: Entity, content: RealityViewContent) {
        /* TODO AOK disabling collision
        print("collision \(enemyEntity.id)")
        priorCollisions[enemyEntity.id] = true
        Task {
            // TODO: dynamically place a spatial audio source... Or
            // have each enemy have a spatial audio source
            await soundManager.playHitSound(entity: lightSaber)
            
            let explosion = Entity()
            explosion.components.set(ParticleEmitterComponent())
            explosion.position = enemyEntity.position
            content.add(explosion)
            
            try? await Task.sleep(for: .milliseconds(500))
            content.remove(enemyEntity)
            
            try? await Task.sleep(for: .milliseconds(500))
            content.add(await enemy.createEnemy())
            
            try? await Task.sleep(for: .milliseconds(500))
            content.remove(explosion)
        }
         */
    }
}



func addPreviewSaber(content:RealityViewContent) async {
    
    // let entity = Model3D(named: "Scene", bundle: realityKitContentBundle)
    if let scene = try? await Entity(named: "Scene", in: realityKitContentBundle) {
        content.add(scene)
    }
}

/*
 #Preview {
 TrainerView(laserSwordModel: LaserSwordViewModel(), soundManager: )
 }
 */

When I start it up, it has a thread fatal error Thread 1: Fatal error: No ObservableObject of type MultipeerManager found. A View.environmentObject(_:) for MultipeerManager may be missing as an ancestor of this view. on the line where I log in the TrainerView.initThe error message you're seeing indicates that SwiftUI is unable to find an ObservableObject of type MultipeerManager when TrainerView is being initialized. This happens because the TrainerView expects an EnvironmentObject of type MultipeerManager, but it hasn't been provided properly up the view hierarchy.

In SwiftUI, when you use @EnvironmentObject, you must ensure that the object is injected into the environment before any view requiring it is loaded. This typically happens using .environmentObject() somewhere in your view hierarchy.

From the code snippet of your LaserSwordTrainerApp, it appears you did add multipeerManager as an environment object to your WindowGroup:

WindowGroup {
    RoutingView()
}.defaultSize(CGSize(width: 250 * 2.0, height: 800 ))
.environmentObject(multipeerManager)

However, if your TrainerView is not directly or indirectly within the RoutingView subtree where multipeerManager is being provided as an EnvironmentObject, it will not have access to it.

To resolve this, ensure that any view hierarchy that leads to TrainerView has multipeerManager available as an EnvironmentObject. For example, if TrainerView is not part of the RoutingView or is used outside the WindowGroup where multipeerManager is provided, you will need to provide the multipeerManager explicitly.

Check how TrainerView is being presented or pushed, and ensure that wherever it's used, it either inherits the environment object or you manually provide it. Here is how you might ensure multipeerManager is available:

  1. If TrainerView is used in another view, say within some navigation flow outside RoutingView, make sure that view also has .environmentObject(multipeerManager) applied.

  2. If TrainerView is directly used in the App structure outside the WindowGroup, apply .environmentObject(multipeerManager) there as well.

For debugging, you can temporarily use @ObservedObject instead of @EnvironmentObject for multipeerManager in TrainerView and pass it directly as a parameter when initializing TrainerView to confirm if the issue resolves. If this works, the problem is definitely with the environment object provisioning in your app structure.In RealityKit there are WindowGroup and ImmersiveSpaces.

If I update to ImmersiveSpace(id: "TrainerSpace") { TrainerView() }.environmentObject(self.multipeerManager)

I still get this errorIf the error persists even after specifying the .environmentObject(self.multipeerManager) directly on the ImmersiveSpace that hosts TrainerView, there could be a few possibilities worth investigating:

  1. Initialization Order Issue: The error might be related to the order in which the multipeerManager is being accessed during the initialization of TrainerView. If multipeerManager is accessed in the initializer of TrainerView before the environment object has a chance to be fully established, this could cause the error.

    To test if this is the case, you can try moving any code that accesses multipeerManager in TrainerView's initializer to a later lifecycle event, like .onAppear.

  2. Correct Scope of Environment Object: Ensure that the scope of the environmentObject modifier encompasses all areas where TrainerView might be initialized or used. It’s crucial that TrainerView is under the hierarchy where multipeerManager is provided as an environment object.

  3. Debugging with OnAppear: Use .onAppear to print or check the multipeerManager inside TrainerView to verify if it's being recognized when the view appears rather than in the initializer.

Here’s how you can adjust the TrainerView initialization to test these points:

struct TrainerView: View {
    
    @EnvironmentObject var multipeerManager: MultipeerManager

    var body: some View {
        Text("Testing Multipeer Manager")
            .onAppear {
                print("TrainerView We've got a multipeerManager \(multipeerManager)")
                // Other initializations that depend on multipeerManager
            }
    }
}

And ensure your ImmersiveSpace is properly configured:

ImmersiveSpace(id: "TrainerSpace") {
    TrainerView().environmentObject(self.multipeerManager)
}

If moving the code to .onAppear resolves the issue, it confirms that the problem is due to the order of operations regarding when the environment object is available versus when it's accessed. You would then need to adjust your design so that any dependency on multipeerManager in TrainerView doesn't kick in before the environment is fully established.

Also, double-check your routing and ensure that there isn’t another path in your navigation or view hierarchy that might lead to TrainerView without passing through the proper environment setup.MultipeerManager is a class that does networking. It sets some variables that I need to react to from UI views. If I start over, what is the best pattern to integrate that with a SwiftUI and RealityKit app?Integrating a networking class like MultipeerManager with a SwiftUI and RealityKit app involves several considerations to ensure that the UI reacts properly to changes in the underlying data or state managed by the networking class. Here’s a suggested approach to achieve a robust and reactive integration:

1. Define MultipeerManager as an Observable Object

Ensure your MultipeerManager class conforms to the ObservableObject protocol and publishes any properties that the UI might need to react to. This allows SwiftUI views to update automatically when these properties change.

import Combine

class MultipeerManager: ObservableObject {
    @Published var isConnected: Bool = false
    @Published var receivedData: String?

    // Other networking code
}

2. Provide MultipeerManager to the SwiftUI Environment

Since MultipeerManager handles networking, it’s likely to be a singleton or similarly long-lived object. You should instantiate it in your app's entry point and inject it into the SwiftUI environment.

@main
struct MyApp: App {
    @StateObject var multipeerManager = MultipeerManager()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(multipeerManager)
        }
    }
}

3. Access MultipeerManager in Your Views

Use the @EnvironmentObject property wrapper in any view that needs to access the MultipeerManager. This way, the view will automatically update when any of the published properties of MultipeerManager change.

struct ContentView: View {
    @EnvironmentObject var multipeerManager: MultipeerManager

    var body: some View {
        Text(multipeerManager.isConnected ? "Connected" : "Not Connected")
        // Additional UI components that depend on multipeerManager
    }
}

4. Handling Asynchronous Tasks

For any asynchronous tasks that need to update the UI once completed, ensure that these updates are dispatched on the main thread, and relevant properties are marked with @Published to refresh the UI.

class MultipeerManager: ObservableObject {
    @Published var isConnected: Bool = false

    func connect() {
        // Example asynchronous network task
        DispatchQueue.global(qos: .background).async {
            // Networking code
            DispatchQueue.main.async {
                self.isConnected = true  // Update on main thread
            }
        }
    }
}

5. Use in RealityKit Views

For views involving RealityKit, ensure that the MultipeerManager is passed down or available in the environment where AR views are constructed. You can use published properties from MultipeerManager to control AR elements or react to changes.

struct ARViewContainer: UIViewRepresentable {
    @EnvironmentObject var multipeerManager: MultipeerManager

    func makeUIView(context: Context) -> ARView {
        let arView = ARView(frame: .zero)
        // Setup using multipeerManager
        return arView
    }

    func updateUIView(_ uiView: ARView, context: Context) {
        // React to changes in multipeerManager
    }
}

6. Testing and Debugging

Make sure to test your application thoroughly to ensure that the UI updates as expected when the state in MultipeerManager changes. This includes both the initial states and any state changes that occur due to networking activities.

By following these steps, you can effectively integrate a networking layer managed by MultipeerManager with a SwiftUI and RealityKit application, leveraging SwiftUI’s environment and reactive features for clean and efficient UI updates.

Provide EnvironmentObject TrainerView created at Tue Jan 20 1970 21:17:22 GMT+0000 (Coordinated Universal Time)

Related Chats

Fix Warning: Use StateObject 0.546

StateObject Error Resolution 0.500

SwiftUI View with Model 0.480

Code Structure Improvements 0.462

Troubleshoot `hasBeenConnected` SwiftUI 0.447

SwApp para Compartir Datos 0.443

Knuckle Position Synchronization 0.442

Thread-Safe Multipeer Manager. 0.434

Find Sub-Component Cylinder 0.417