Update, Content, and Attachments on Vision Pro

I have a scene setup that uses places images on planes to mimic an RPG-style character interaction. There's a large scene background image and a smaller character image in the foreground. Both are added as content to a RealityView. There's one attachment that is a dialogue window for interaction with the character, and it is attached to the character image. When the scene changes, I need the images and the dialogue window to refresh. My current approach has been to remove everything from the scene and add the new content in the update closure.

    
    @EnvironmentObject var narrativeModel: NarrativeModel
    @EnvironmentObject var dialogueModel: DialogueViewModel
    
    @State private var sceneChange = false
    
    private let dialogueViewID = "dialogue"
    
    
    var body: some View {
        
        RealityView { content, attachments in

            //at start, generate background image only and no characters
            if narrativeModel.currentSceneIndex == -1 {
                
                content.add(generateBackground(image: narrativeModel.backgroundImage!))
                
            }
            
        } update : { content, attachments in
            
            print("update called")
                
            if narrativeModel.currentSceneIndex != -1 {
                print("sceneChange: \(sceneChange)")
                if sceneChange {
                    
                    //remove old entitites
                    if narrativeModel.currentSceneIndex != 0 {
                        content.remove(attachments.entity(for: dialogueViewID)!)
                    }
                    content.entities.removeAll()
                    
                    //generate the background image for the scene
                    content.add(generateBackground(image: narrativeModel.scenes[narrativeModel.currentSceneIndex].backgroundImage))
                
                    //generate the characters for the scene
                    let character = generateCharacter(image: narrativeModel.scenes[narrativeModel.currentSceneIndex].characterImage)
                    
                    content.add(character)
                    print(content)
                    if let character_attachment = attachments.entity(for: "dialogue"){
                        print("attachment clause executes")
                        character_attachment.position = [0.45, 0, 0]
                        character.addChild(character_attachment)
                    }

                }
            }
            
        } attachments: {
                
            Attachment(id: dialogueViewID){
                DialogueView()
                    .environmentObject(dialogueModel)
                    .frame(width: 400, height: 600)
                    .glassBackgroundEffect()
            }
            
        }
        
        //load scene images
        .onChange(of:narrativeModel.currentSceneIndex){
            
            print("SceneView onChange called")
            DispatchQueue.main.async{
                self.sceneChange = true
            }
            print("SceneView onChange toggle - sceneChange = \(sceneChange)")
        }
    }
    

If I don't use the dialogue window, this all works just fine. If I do, when I click the next button (in another view), which increments the current scene index, I enter some kind of loop where the sceneChange value gets toggled to true but never gets toggled back to false (even though it's changed in the update closure). The reason I have the sceneChange value is because I need to update the content and attachments whenever the scene index changes, and I need a state variable to trigger the update function to do this. My questions are:

  1. Why might I be entering this loop? Why would it only happen if I send a message in the dialogue view attachment, which is a whole separate view?
  2. Is there a better way to be doing this?
Answered by Vision Pro Engineer in 802526022

@dlonzell

Apologies for the latency.

I have, but then the problem is that I can't get access to content outside of the make or update closure, to my knowledge.

You can create a root entity as a state variable, add that to content, then add all children to that root entity. Since the root entity is a state variable it will be available on update.

why would update keep firing based on an interaction with an attachment? I though it only happens when state changes?

As you mentioned render is called anytime state changes. The attachment has a binding to $dialogueModel.currentInput so changing it changes the state and causes a rerender.

Quick Edit: I was debugging and forgot to add the statement to the provided code, at the beginning of the "if sceneChange { ... conditional.

DispatchQueue.main.async{
  self.sceneChange = false 
}

This still gives the same error, I just don't know how to edit the original post so it shows the right code.

Hi @dlonzell

You should try to avoid updating state during a view update. This usually triggers a runtime warning that reads "Modifying state during view update, this will cause undefined behavior".

It looks as if you want to re-render the view as a player moves through your game. One way to do this is to use a value that doesn't require reseting to change continuously. Here's how you can approach this with an enum.

Define the enum.

enum GameScenes {
    case someInitialScene
    case someOtherScene
}

On your view, replace

@State private var sceneChange = false

with

@State var currentScene = GameScenes.someInitialScene

As the user advances through your game, change the enum value instead of toggling a boolean. Then there's no need to reset. For example:

currentScene = GameScenes.someOtherScene

Keep in mind the view updates anytime its state changes; if your render logic is a function of the current state variables, you may not need the enum or boolean at all.

Generally you want to avoid explicitly triggering view updates and rely on state changes to trigger them.

Here are a couple resources to better understand state management in SwiftUI.

I need the view to update every time the index changes, but only once. The reason I try to check and re-set scene change is because I don't want it to re-render the same content every time update is called, since it's getting called when I enter messages in the attachment, and other times.

Is it not an issue for the view to re-render every time update is called? (right now this means removing current scene images and adding the corresponding images for the 'new' scene) I'm not sure why update is called on a loop whenever I start to enter text into the dialogue view.

Here's the code for DialogueView in case that's helpful:

struct DialogueView: View {
    @EnvironmentObject var dialogueModel: DialogueViewModel
    var body: some View {

        VStack{
            ScrollView{
                ForEach(dialogueModel.messages.filter({$0.role != .system}), id: \.id){ message in
                    messageView(message: message)
                    Spacer()
                }
                
            }
            HStack{
                TextField("Enter a message...", text: $dialogueModel.currentInput)
                Button{
                    dialogueModel.sendMessage()
                } label: {
                    Text("Send")
                }
            }
        }
        .padding()

    }
    
    func messageView(message: Message) -> some View{
        HStack{
            if message.role == .user {Spacer()}
            Text(message.content)
            if message.role == .assistant {Spacer()}
        }
    }
}

@dlonzell

Have you considered updating the scene in onChange while leaving update to handle the attachments?

Accepted Answer

@dlonzell

Apologies for the latency.

I have, but then the problem is that I can't get access to content outside of the make or update closure, to my knowledge.

You can create a root entity as a state variable, add that to content, then add all children to that root entity. Since the root entity is a state variable it will be available on update.

why would update keep firing based on an interaction with an attachment? I though it only happens when state changes?

As you mentioned render is called anytime state changes. The attachment has a binding to $dialogueModel.currentInput so changing it changes the state and causes a rerender.

Update, Content, and Attachments on Vision Pro
 
 
Q