Reading Time: 12 minutes

 

Welcome back! If you’re returning from the first blog in this series, great to have you back! If you haven’t yet read the previous posts, I highly recommend checking it out A Practical Guide For visionOS Development using TCA (Part #1), as this series builds upon prior knowledge.

In this post, we’ll continue our journey by developing another application for visionOS. Specifically, we’ll be building the “Spaces” application, leveraging The Composable Architecture framework to structure our code.

From this point forward, we’ll be working exclusively within the provided Starter projects. We’ll assume you’re familiar with setting up a project, adding necessary packages, and constructing basic SwiftUI elements, as those foundational steps won’t be covered in this tutorial.

Spaces App:

Get the projects from:

TODO: #1

Let’s begin by getting familiar with two key environmental actions that control the flow of immersive space presentations: openImmersiveSpace and dismissImmersiveSpace. As their names suggest, one is responsible for presenting an immersive space, while the other dismisses the currently active one. Now, navigate to the SpaceView folder, open the SpaceView.swift file, and replace TODO: #1 with the following line of code:

Copy to Clipboard

 

TODO: #2

Next, replace TODO: #2 with this line of code:

Copy to Clipboard

 

Let’s take a moment to break down what these actions do in detail.

  • openImmersiveSpace: This environmental value, combined with an Immersive Window ID (which we’ll discuss shortly), manages the presentation of a specific immersive space.
  • dismissImmersiveSpace: Unlike openImmersiveSpace, this action dismisses the currently open immersive space and doesn’t require an Immersive Window ID.

Since we’re handling immersive space presentations within the SpacesStore, navigate to the SpacesStore.swift file to prepare it for managing these presentations. Shortly, we’ll pass the environmental values we just introduced to our store.

TODO: #3

let’s define a type that represents the various states our immersive spaces can be in, such as closed, inTransition, or open. Soon, we’ll create an instance of this type to track the current state of our immersive space.

Go ahead and create an enumeration ImmersiveSpaceState with the following cases: closed, inTransition, and open. Feel free to copy and paste the following code snippet:

Copy to Clipboard

 

TODO: #4

Go ahead and create an instance of the type we just defined. Since there isn’t an active immersive scene at the start, initialize this property with the closed state:

Copy to Clipboard

 

TODO: #5

If your application allows users to interact with multiple immersive spaces, you may encounter a bug where users can open the same space multiple times or even open other spaces while one is already active. To prevent this, we’ll introduce another property that tracks the current active immersive space. This property will store the immersive space’s windowId, which we’ll later use to disable any buttons responsible for presenting other immersive spaces while one is already active. Replace this todo mark with the following code snippet:

Copy to Clipboard

 

TODO: #6

Now it’s time to implement the actions triggered from our SpaceView. Navigate to the Action enumeration and replace TODO: #6 with the following code snippet:

Copy to Clipboard

 

After adding the action cases, Xcode will likely show an error like “Switch must be exhaustive.” You can safely ignore this for now, as I’ll explain what each case does in detail:

  • openImmersiveSpace(String, OpenImmersiveSpaceAction): This action takes two parameters. The first is a String where you’ll pass the windowID for the immersive space, and the second is an OpenImmersiveSpaceAction where you’ll pass the openImmersiveSpace environmental value from the SpaceView.
  • dismissImmersiveSpaceAction(DismissImmersiveSpaceAction): This action handles dismissing the currently presented immersive space. You’ll pass the dismissImmersiveSpace value from your SpaceView.
  • changeImmersiveSpaceState(ImmersiveSpaceState): This is an internal event used within SpacesStore to update the immersiveSpaceState.
  • setWindowID(String): This action is also internal to the store and is used to update the activeWindowId property.

TODO: #7

Let’s add the missing cases as Xcode suggests. Simply click on “Fix” to implement them.

We are going to begin with the simplest case: setWindowID. Xcode will have implemented this case, but it doesn’t yet store the parameter received with the action. Since we need to store the active window ID, we’ll update the case using case let:

Copy to Clipboard

 

Inside the body of this case, we’ll update the current state by assigning the windowId to the activeWindowId property. Since there are no side effects to handle, we can return .none from this case.

Copy to Clipboard

 

Great! Now let’s move on to another simple case: changeImmersiveSpaceState. We’ll follow a similar process, updating the state with the new immersive space state:

Copy to Clipboard

 

Fantastic! We’ve now covered the simplest, yet essential, events that can occur in our SpacesView. Next, let’s handle presenting and dismissing immersive spaces using the environmental values we set up earlier in this blog.

For the dismissImmersiveSpaceAction case, start by storing the dismiss action using:

Copy to Clipboard

 

Then, implement the necessary logic inside the case.

Copy to Clipboard

 

Here’s a step by step breakdown:

1. case let .dismissImmersiveSpaceAction(dismiss):

  • This is part of a reducer that handles the dismissImmersiveSpaceAction action.
  • The dismiss value is passed into the case. It’s likely an @escaping closure or function that handles dismissing the immersive space.

2. return .run

  • .run is a convenience method in TCA that lets you run asynchronous effects or tasks in response to actions. It provides a cleaner way to handle side effects, such as making network requests or interacting with system APIs.
  • In this case, the side effect is dismissing an immersive space and updating the state accordingly.

3. [immersiveSpaceState = state.immersiveSpaceState]

  • This captures the immersiveSpaceState from the current state so it can be used inside the run block.
  • The square brackets ([]) are part of a capture list, which allows capturing values from the surrounding context to use within the closure.

4. send in

  • The closure passed to .run accepts a single parameter, send, which is a function used to send actions back into the reducer to update the state.
  • The send function is async in this case because it allows sending actions after awaiting the completion of other asynchronous work.

5. switch immersiveSpaceState { … }

  • A switch statement is used to handle the current state of the immersive space (.open, .closed, or .inTransition).

6. case .open:

  • If the immersive space is currently open, the following tasks are performed asynchronously:
  • await dismiss(): The immersive space is dismissed by calling the dismiss() closure. This is the core action of this case.
  • await send(.changeImmersiveSpaceState(.closed)): After dismissing the space, the reducer sends the changeImmersiveSpaceState(.closed) action to update the immersiveSpaceState to closed.
  • await send(.setWindowID(“”)): It also sends the setWindowID(“”) action to reset the windowID back to an empty string, indicating that no immersive space is active.

7. case .closed, .inTransition:

  • If the immersive space is already closed or in the process of transitioning (.inTransition), nothing is done. These states don’t require dismissing the space, so the break statement ensures no further action occurs.

Finally, let’s implement the logic for presenting an immersive space. Use the following code snippet to replace case .openImmersiveSpace(_, _).

Copy to Clipboard

 

Let’s breakdown what we just did:

1. case let .openImmersiveSpace(windowId, open):

  • This case handles opening an immersive space. It takes two parameters: windowId (the ID of the space to be opened) and open (an action that triggers the opening of the immersive space).

2. switch immersiveSpaceState:

  • We first check the current state of the immersive space. If the space is already open or in transition, nothing needs to happen.
  • We’re interested in the case where the space is currently closed.

3. case .closed:

  • If the immersive space is closed, the process begins.
  • First, we send the changeImmersiveSpaceState(.inTransition) action to indicate that the space is in the process of opening.

4. switch await open(id: windowId):

  • We then use the open action to attempt opening the immersive space, passing in the windowId.
  • The result of this open action can be one of three outcomes: opened, userCancelled, or error.

5. case .opened:

  • If the space is successfully opened, we:
  • Send the action changeImmersiveSpaceState(.open) to update the state to open.
  • Send the action setWindowID(windowId) to store the windowId of the active immersive space.

6. case .userCancelled, .error:

  • If the opening process is cancelled by the user or fails with an error, we use fallthrough to ensure the state is reset to closed.

7. @unknown default:

  • This handles any unexpected or future outcomes by resetting the state to closed.

8. default: break:

  • If the immersive space is not in the closed state when this action is triggered, we don’t need to take any further action, so we simply break.

Congratulations! You’ve successfully set up the store. Now, it’s time to bring everything together by returning to the SpacesView.swift file.

In SpacesView.swift, replace TODO #8 with:

Copy to Clipboard

 

This property will track whether the immersive space is currently open or not. We’ll use it to decide when to present or dismiss an immersive space. Additionally, this Boolean value will help determine which buttons should remain interactive when an immersive space is being displayed.

Next, replace TODO #9 with the following line:

Copy to Clipboard

 

This ensures that buttons are disabled if an immersive space is already open and doesn’t match the active window ID, preventing multiple immersive spaces from opening at the same time.

Finally, go ahead and replace TODO #10 with the following code snippet:

Copy to Clipboard

 

This code is a simple conditional block that handles whether an immersive space should be opened or dismissed, based on whether it is currently open. Let’s break it down:

  • If the immersive space is already open (immersiveIsOpen == true), the code sends an action to dismiss it.
  • If the immersive space is not open (immersiveIsOpen == false), the code sends an action to to the store to change the app state and open the immersive space.

TODO: #11

We need to set up our immersive spaces for individual Immersive space presentation. To achieve this, each immersive space must be handled within a RealityView. Navigate to the ParkingLotSpace folder and open the ParkingLotSpaceView.swift file. Replace TODO #11 and TODO #12 with the following imports to include the necessary libraries for using RealityView.

Copy to Clipboard

 

Before we add a RealityView to this view, we need to configure some elements within the ParkingLotSpaceStore. Open the ParkingLotSpaceStore.swift file, where I’ve already imported the required libraries.

A RealityView is essential for presenting immersive visionOS spaces because it provides the rendering engine that overlays 3D content onto the real world, enabling users to interact with virtual environments seamlessly. It manages the camera feed and AR session, allowing for rich, immersive experiences that enhance user engagement in AR applications.

Replace TODO #12 with the following line:

Copy to Clipboard

 

This property will later be added as a child to the content property of our RealityView in ParkingLotSpaceView. By having this property, we can manage and maintain the logic for adding immersive spaces and other 3D objects within the scope of our store.

Navigate to the Action enum and replace TODO #13 with:

Copy to Clipboard

 

This action will be used later in our view to trigger the loading of a scene.

While we’re in the Action enum, let’s implement another necessary event by replacing TODO #14 with:

Copy to Clipboard

 

Now, for TODO #15, we need to exhaust our actions switch statement with the new cases we’ve just created.

Go ahead and insert the following code in the .loadScene case:

Copy to Clipboard

 

Let’s break it down:

In the .loadScene case, we handle the loading of an immersive 3D scene. This action is responsible for fetching a specific entity, in this case, a ParkingLot, from a RealityKit content bundle and updating our app’s scene accordingly.

1. Loading the Scene:

The .run function allows us to perform asynchronous tasks. In this case, we use it to asynchronously load a 3D entity (our ParkingLot) from the realityKitContentBundle using the Entity(named:in:) method. This method looks for an entity named “ParkingLot” in the specified content bundle and returns it.

2. Error Handling:

We use try? to handle any errors that might occur during the loading process. This ensures that if the entity can’t be found or loaded, the app doesn’t crash; instead, it simply skips this block of code.

3. Updating the Scene:

If the entity is successfully loaded, the ParkingLot is sent to the next action, updateSceneContent(parkingLot), which updates our app’s state with the newly loaded scene. This action ensures that the loaded content is rendered in the immersive space, allowing users to interact with it.

Lastly, using the case let pattern, we capture the content received from the .loadScene event and add it to the content property of our state. This ensures the new scene is properly integrated into the existing state. Here’s how we do it:

Copy to Clipboard

 

By calling addChild(content), we ensure that the newly loaded content becomes part of the current scene, making it ready for display and interaction. Since we’re not expecting any side effects from this event, we simply conclude it by returning .none, indicating no further actions are required.

TODO: #16

It’s time to implement the RealityView. Head back to the ParkingLotSpaceView.swift file and replace TODO: #16 with the following code:

Copy to Clipboard

 

This block of code should be straightforward, but here’s a brief recap:

  • RealityView { content in … }: This creates a RealityView, and the closure allows us to add the store.content to it, which holds the immersive scene content.
  • .task { store.send(.loadScene) }: This triggers the .loadScene event as soon as the RealityView appears, ensuring that the scene is loaded and presented to the user.

With this in place, the scene will be displayed in the RealityView when the view is rendered.

Next, it’s time to repeat the same steps for both StationSpaceView.swift and StationSpaceStore.swift. The only change you’ll need to make is renaming “ParkingLot” to “TrainStation.”

Simply follow the same process, adjusting for the new entity name, and you’re all set!

For your convenience, here are the code blocks for both StationSpaceView.swift and StationSpaceStore.swift:

By incorporating the following code snippets, you’ll be completing and bypassing tasks #17 through #23.

Copy to Clipboard
Copy to Clipboard

 

Congratulations, we’re almost at the finish line! Just a few final touches, and you’ll be able to build and run your app for the first time.

For the last steps, let’s head over to SpaceApp.swift. Here, we’ll define our Immersive Spaces using their designated IDs. Now, if you’re wondering where these IDs come from, you can revisit SpacesStore.swift, where I’ve already defined the SpacesModels. You’ll see two spaceWindowID values — one for StationSpace and one for ParkingLot. These are the IDs you’ll use for your ImmersiveSpace IDs in SpacesApp.swift.

And yes, you might be thinking, “Isn’t there a better way to share these values across the app?” The answer is: of course! You could define a public constant struct or enum, but since that wasn’t the main focus of this blog, I politely declined to take that extra step 😄.

But before we get sidetracked, let’s return to SpacesApp.swift.

First, I’d like to make sure the app’s main window doesn’t shrink below the content size, so we’ll add the .windowResizability(.contentSize) modifier to TODO #23.

Copy to Clipboard

 

Next, replace TODO: #24 and TODO: #25 with the following lines of code:

Copy to Clipboard

 

The ImmersiveSpace is a key structure used to define and manage immersive AR experiences in visionOS. Each ImmersiveSpace is identified by a unique id, allowing the app to distinguish between different immersive environments, like a “TrainStation” or “ParkingLot”.

Key Aspects:

  • id: The id is a string identifier that uniquely represents each immersive space. It allows the system to differentiate between various immersive environments within the app.
  • .immersionStyle(selection: .constant(.progressive), in: .progressive): This modifier controls the style of immersion. In this case, it is set to progressive, which means the user will be gradually immersed into the AR space, offering a smooth transition into the immersive environment.
  • .upperLimbVisibility(.visible): This modifier ensures that the user’s arms and hands are visible in the immersive space, which enhances the interactivity and makes the AR experience feel more natural and engaging.

And that’s it! It is time to hint “Run”!

Summary

In this post, we continued building the Spaces application for visionOS using The Composable Architecture (TCA). We focused on managing immersive spaces with actions such as presenting and dismissing these spaces, using environmental values like openImmersiveSpace and dismissImmersiveSpace. We also set up ImmersiveSpaceState to track whether an immersive space is open, closed, or in transition.

Additionally, we implemented logic for loading and displaying immersive scenes using RealityKit, introducing RealityView to manage 3D AR content. By defining unique space IDs like “TrainStation” and “ParkingLot” in SpaceApp.swift, we ensured proper identification and rendering of these immersive spaces.
We wrapped up by configuring the immersive experience with .immersionStyle(.progressive) for smooth transitions and .upperLimbVisibility(.visible) to enhance user interaction.

And with that, your app is now ready to run its immersive AR spaces!

Stay Tuned!

In the next part of the series, we’ll dive into handling Windows and Window placements in visionOS. These topics will further expand your understanding of how to create dynamic, interactive visionOS applications.

Written by Yasser Farahi

Software Engineer

Yasser is a real apple fanboy, so the word passion falls short when it comes to the relationship between Yasser and iOS development. Yasser believes that developers should always be learning. He teaches this philosophy and iOS development one day a week at the media college of Amsterdam.

Published on: October 12th, 2024

Newsletter

Become an app expert? Leave your e-mail.

nederlandse loterij en egeniq
pathe thuis en egeniq
rpo and egeniq
mvw en egeniq
rtl and egeniq