RealityView update closure

Apple docs for RealityView state:

You can also use the optional update closure on your RealityView to update your RealityKit content in response to changes in your view’s state."

Unfortunately, I've not been able to get this to work.

All of my 3D content is programmatically generated - I'm not using any external 3D modeling tools. I have an object that conforms to @ObservableObject. Its @Published variables define the size of a programmatically created Entity. Using the initializer values of these @Published variables, the 1st rendering of the RealityView { content in } works like a charm.

Basic structure is this:

var body: some View {
RealityView { content in
    // create original 3D content using initial values of @Published variables - works perfect
} update: { content in
    // modify 3D content in response to changes of @Published variables - never works
}

Debug statements show that the update: closure gets called as expected - based upon changes in the viewModel's @Published variables. However, the 3D content never changes - even though the 3D content is based upon the @Published variables.

Obviously, if the @Published variables are used in the 1st rendering, and the update: closure is called whenever changes occur to these @Published variables, then why isn't the update: closure updating the RealityKit content as described in the Apple docs?

I've tried everything I can think of - including removing all objects in the update: closure and replacing them with the same call that populated them in the 1st rendering. Debug statements show that the new @Published values are correct as expected when the update: closure is called, but the RealityView never changes.

Known limitation of the beta? Programmer error? Thoughts?

Accepted Reply

Well, as seems to be all too often and embarassingly the case - ask a question in public, and suddenly the answer reveals itself...

As it turns out I wasn't actually removing all objects in the update: closure and then replacing them - I was adding rebuilding them in my graph but not in the RealityView update: closure. Dumb oversight.

TL;DR: I was expecting the @Published values to automagically update the 3D content - much the same way that SwiftUI automatically updates 2D textfields and such. That isn't what happens in the RealityView update: closure.

So, the solution was simple:

} update: { updateContent in
     updateContent.entities.removeAll()
     graph.renew()
     for element in graph.all {
         updateContent.add(element)
     }
}

where graph is a custom object that contains my RealityKit scene.

Replies

Well, as seems to be all too often and embarassingly the case - ask a question in public, and suddenly the answer reveals itself...

As it turns out I wasn't actually removing all objects in the update: closure and then replacing them - I was adding rebuilding them in my graph but not in the RealityView update: closure. Dumb oversight.

TL;DR: I was expecting the @Published values to automagically update the 3D content - much the same way that SwiftUI automatically updates 2D textfields and such. That isn't what happens in the RealityView update: closure.

So, the solution was simple:

} update: { updateContent in
     updateContent.entities.removeAll()
     graph.renew()
     for element in graph.all {
         updateContent.add(element)
     }
}

where graph is a custom object that contains my RealityKit scene.

Hi @CryptoKoa . I am having the exact same issue! It seems like you've come to a solution, but there's one part of your solution I'm unable to grasp. What is this graph object? Could you please post a full code snippet?

Context: I am also building a reality view that uses @Published variables to update its state, but when i change the @Published variables, the reality view doesn't re-render. I would greatly appreciate some help.

Add a Comment
``struct CubeView: View {
    
    private var number: Int

    init(number: Int) {
        self.number = number
    }
            
    var body: some View {
        let boxSize: Float = 0.1
        let cornerRadius: Float = boxSize / 10
        let cubeColorTheme = CubeColorTheme.colorPair(for: number)

        
        RealityView { content in
            let _ = print("\t\tCreating reality view with \(number)")
            let boxEntity = ModelEntity(
                mesh: .generateBox(size: boxSize, cornerRadius: cornerRadius),
                materials: [
                    SimpleMaterial(color: UIColor(cubeColorTheme.background), isMetallic: false)
                ])
            
            let textEntity = ModelEntity(
                mesh: MeshResource.generateText(
                    title(),
                    extrusionDepth: 0,
                    font: .systemFont(ofSize: fontSize(), weight: .semibold),
                    containerFrame: CGRect(),
                    alignment: .center,
                    lineBreakMode: .byCharWrapping
                ),
                materials: [
                    SimpleMaterial(color: UIColor(cubeColorTheme.font), isMetallic: false)
                ]
            )
            
            textEntity.position = textPosition()
            boxEntity.addChild(textEntity)
            content.add(boxEntity)
        } update: { content in
            let _ = print("Number updated") // PRINTS FINE
        }
    }``

Here is my CubeView, which has a RealityView inside of it. I then display this on my window using the following code:

                VStack {
                    ForEach(viewModel.matrix, id: \.id) { cube in
                        CubeView(number: cube.value)
                    }
                }

I have some other code that changes the number on a cube (Not the ID) and for some reason the cube does not re-render with the new number _but_ the update closure gets called as expected!

Hey, thanks for posting your answer! I was wondering if you could give more detail on what your graph object looks like?

I currently have a similar issue, I'm generating RealityView objects via a ForEach loop. I pass in an identifiable object into the RealityView and render the view based on the contents of the object. However, the reality view isn't rerendering my object's content even though print statements show that the correct values have been passed in.

Getting really lost here unfortunately :(

Still unsure about what the use of the update closure actually is.

The documentation is particularly bad about RealityView. There is no clear guidance on what you should do in the make/update/attachment closure, the order in which these closures are executed, etc.

Based on my experience - you can't expect change to environment objects to trigger the update closure. This is REALLY SURPRISING. My workaround is to use another view that wraps the reality view and listens to that environment object, which also passes the listened object as a prop to the reality view. In a reality view, any change in @State or a prop should trigger the update closure.

The update closure is also called multiple times at the beginning of the lifecycle, which I have no idea why. That was not documented anywhere. When does the entities in the attachment closure become visible to the other closures? Also no clue.

If someone from Apple is reading - this RealityView is the single most important concept introduced in RealityKit2. It's the entrance to everything and really worth more documentation and more examples.

Here’s a simple implementation of my Graph object. Basic ideas are just this:

  1. the Graph class conforms to @ObservableObject and contains an @Published Float called position. My app can manipulate this and RealityKit will reposition the object based upon this.
  2. the renew() function creates an AnchorEntity, loads a jpg to be used as a Material, creates a box Entity using .generateBox(), and then adds this as a child of the anchor
  3. renew() also resets / re-creates an array of all Entities used (called all) by appending each child of the anchor defined in step (2). I manually maintain this array to facilitate app logic outside of the RealityView update: closure

Again, this is a simplified implementation but it shows how I’m manually creating the RealityView scene with each call to the update: closure. Surely this isn’t the most efficient way to update a scene, but it’s all I can figure with existing documentation.

import SwiftUI
import RealityKit

class Graph: ObservableObject {
// step 1: define an @Published var
	@Published var position:   Float = -1.0
	
	var anchor:	AnchorEntity = AnchorEntity()
	var all:		[Entity] = []
	
	static let singleton = Graph()
	
	init() {
		renew()
	}
	
	func renew() {
    // step 2: manually create the scene
    // create / recreate anchor 
		anchor = AnchorEntity()
		
    // load a jpg to paint the cube
		guard let resource = try? TextureResource.load(named: "gates") else {
			fatalError("Unable to load texture.")
		}
		var material = UnlitMaterial()
		material.color = .init(texture: .init(resource))
		
		// create a box and add material to it.
		let entity = Entity()
		entity.components.set(ModelComponent( mesh: .generateBox(size: 0.4, cornerRadius: 0.05), materials: [material] ))
		entity.name = "box"

     // use the @Published var to position in the scene
		entity.position.z = position

    // add the entity as a child to the anchor
		anchor.addChild(entity)
		

    // step 3: maintain my own array of all entities to facilitate my own app’s logic
		all = []
		for e in anchor.children {
			all.append(e)
		}
	}
}

Maybe I'm confusing something, but the way I add models to my RealityView, is to have a rootEntity, that's managed by my ViewModel, which within I just add more entities. I don't use any anchors anywhere as of now.

  • Yeah, I’m not claiming this is the only way to do things, nor even the correct way TBH - it’s just what worked for me w/in the confines of my app’s requirements. YMMV, for sure.

Add a Comment