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:
If
TrainerView
is used in another view, say within some navigation flow outsideRoutingView
, make sure that view also has.environmentObject(multipeerManager)
applied.If
TrainerView
is directly used in theApp
structure outside theWindowGroup
, 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:
Initialization Order Issue: The error might be related to the order in which the
multipeerManager
is being accessed during the initialization ofTrainerView
. IfmultipeerManager
is accessed in the initializer ofTrainerView
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
inTrainerView
's initializer to a later lifecycle event, like.onAppear
.Correct Scope of Environment Object: Ensure that the scope of the
environmentObject
modifier encompasses all areas whereTrainerView
might be initialized or used. It’s crucial thatTrainerView
is under the hierarchy wheremultipeerManager
is provided as an environment object.Debugging with OnAppear: Use
.onAppear
to print or check themultipeerManager
insideTrainerView
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.
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