Hello @just_some_game_dev,
I believe that starting with understanding SwiftUI is key to this question. I'd strongly encourage you to checkout the following resources in this category:
I'd defer to the Observe model data in a view section to answer much of your questions. As this section explains, breaking your view into views helps the parent views not update when a property inside a child view updates.
SwiftUI recomputes the body of the view any time a dependency that body reads changes. This is triggered by @State changes, properties on @Observable models, environment changes and bindings. Invalidation flows downwards to child views; a child's changing state does not trigger the parent's body to re-evaluate. SwiftUI is smart in that a child view whose dependencies did not change doesn't recompute the body. Apply the @Observable macro on your model to enable your view to observe the individual properties that your view body reads. With the older @ObservedObject, every change to the object invalidates the view causing the body to be reevaluated.
Use @State to establish the source of truth for the view. The view can update this value and the value is retained when a parent's body reevaluation causes the view to be recreated. It's important to note that the @State initializers run on every creation of the view, so keep these as lightweight as possible. Consider using a .task to perform any heavy initialization to @State values.
You can pass observable objects to children views essentially for free as long as you aren't reading their properties and the child views do not have heavy state initialization. Just passing them through intermediate views does not retrigger a body reevaluation. Similarly changes to the environment triggers a body reevaluation only when you read an environment value.
Now for the RealityKit portion of this question, RealityKit has a RealityView(make:update:) function. The update closure will be called so that you can update the RealityView content when the view’s state changes. Don't update the view's state in this update closure. Consider not providing an update closure and instead monitoring for changes using the .onChange modifiers.
Canyon Crosser uses this approach to update the RealityView content for boolean changes, AppState changes, and changes on an entity. Introduced last year, Entity.Observable allows you to react to individual properties like the position (entity.observable.position), or whole components (entity.observable.components[HikePlaybackStateComponent.self]?.isPaused).
When you do this you are observing the component as an object, similar to @ObservableObject. Migrating from the Observable Object protocol to the Observable macro is a great article that goes over the difference between the two. In Canyon Crosser we split up hike state into three components: HikePlaybackStateComponent, HikerProgressComponent, and HikeTimingComponent. This was done, so that a view observing the paused state (PlayPauseButton) would only update when HikePlaybackStateComponent.isPaused is updated and not at every frame when HikerProgressComponent.progress is updated.
Running instruments is always advisable to understand where you might consider making additional performance updates to your application. Consider watching Optimize SwiftUI performance with Instruments to learn more about the SwiftUI instrument introduced last year. There are also a few tools that you can reach for temporarily to understand when a view updates. You can put Self._printChanges() or Self._logChanges() in the body of the view to understand when a view body is reevaluated. You can also visualize this with a flashing background color, using something like this: .background { Color(white: [1.0, 0.0].randomElement()!) }.
I hope this helps and gives you a broader understanding of how updates are triggered. Please mark this answer as accepted if this answers your questions. Is there anything that have questions about or specific details you are considering for your data model?
Thanks,
Michael