Reading Time: 13 minutes
Welcome to Our visionOS Development Journey!
I’m excited to have you here as we dive into the world of visionOS development! Over the course of this blog series, we’ll explore some key topics that will guide you through building visionOS apps using The Composable Architecture (TCA), all while focusing on visionOS-specific features.
Each blog post will provide both a starting project and a final project so you can follow along and see how the app evolves step by step.
Here’s a quick look at the roadmap for this series:
- Ticker App: We’ll start by building a simple visionOS app. This will introduce you to how a basic visionOS app is structured and how TCA plays a role in its architecture.
- Spaces: In this post, we’ll dive into how to load and manage immersive scenes. You’ll learn how to handle immersive environments effectively.
- Months: We’ll explore how to open and manage multiple windows, a core aspect of visionOS applications.
- Planets: Finally, we’ll combine everything we’ve learned to create a portal view app. This will allow us to bring together immersive environments and window management into a cohesive experience.
Each of these blogs will build on the knowledge gained in the previous posts, and by the end, you’ll have a strong foundation in visionOS development using TCA.
In this blog series, we’re using The Composable Architecture (TCA) to organize our code for iOS development. TCA is a library built to help manage state, handle side effects, and compose complex logic in a clear and structured way. It follows a functional programming approach, making code predictable and reusable by breaking it into small, composable pieces.
The reason I chose TCA for structuring my code is its ability to scale as an application grows. TCA’s unidirectional data flow and clear separation of concerns create a solid foundation, which not only makes the app easier to maintain but also reduces the cognitive load when working on complex features. Its focus on testability and predictable behavior makes it an ideal fit for modern app development, particularly in scenarios where managing state can quickly become overwhelming.
If you’re interested in learning the technical details of TCA, I highly recommend the Point-Free video series, where the creators go into great depth about how it works and how to implement it.
However, our blog series will only focus on some of the important topics related to visionOS development.
Ticker App
In this blog, i’ll walk you through creating a visionOS project from scratch. If you’re already comfortable with setting up Xcode projects, feel free to skip the Create Project section. You can either create your own project or use the provided starter project. Once ready, jump straight to the Implementation section and follow along from there.
Get the projects from:
> Create Project Section
1. Launch Xcode and Create a New Project
- In the navigation menu, select visionOS. Under the Application section, choose App and click Next.
- Name your app Ticker and add an Organization Identifier, e.g., com.yourdomain
- Since we’ll be working with windowed apps in this series, select Window for the Initial Scene. As we’re not handling 3D spaces yet, set Immersive Space Renderer to None, then hit Next.
2. Initial Setup Before Coding
Before we start writing code, we need to set up a few things.
- In Xcode, go to the File menu and click on Add Package Dependencies.
- In the new window, paste the following URL into the search field:
https://github.com/pointfreeco/swift-composable-architecture
- Once you see the package, click Add Package. In the next window, under Add to Target, select your project and click Add Package.
> Implementation Section
1. Remove ContentView.swift
Since we’ll be building our own view, you can remove the ContentView.swift file from the project.
2. Create HomeView.swift
• Create a new SwiftUI view called HomeView.
• Right after the import SwiftUI statement, add:
import ComposableArchitecture
Copy and paste the following code snippet inside your HomeView.swift file:
Xcode might start showing several warnings and errors — don’t worry, ignore them for now. These errors are likely because your project isn’t fully aware of the ComposableArchitecture yet, or because your HomeView doesn’t know about
3. Create HomeStore.swift
Next, create an empty Swift file called HomeStore.swift. The later HomeView will report events to HomeStore, which will handle state changes and re-render the view based on the new state.
• In HomeStore.swift, import SwiftUI and ComposableArchitecture.
• Then, copy and paste the following code snippet inside the file:
Let’s break down the code step by step, explaining what each part does.
1. @Reducer
This annotation marks the HomeStore structure as a reducer, which is responsible for handling actions and updating the app’s state accordingly. The reducer is at the core of the Composable Architecture pattern, acting as a function that takes the current state and an action, and returns a new state.
2. struct HomeStore
The HomeStore structure is where we define the app’s state, the possible actions that can affect the state, and the logic that modifies the state when an action is received. This acts as the “brain” of our app.
3. @ObservableState
The @ObservableState annotation is used to make the app’s state observable, meaning whenever the state changes, the UI will automatically re-render to reflect the new state. The state is wrapped inside a struct named State, which can conform to the Equatable protocol. This means that Swift will be able to compare two instances of State and determine if they’re equal, which helps with making the state testable.
4. enum Action
The Action enum represents all the possible user interactions or events that can happen in the app, such as button taps or data changes. Each case inside the Action enum corresponds to an event that the app needs to handle. Similar to the state, Action also can conform to the Equatable protocol.
5. var body: some ReducerOf
This is the core of the reducer. It’s where the logic to handle actions and update the state lives. The body is a computed property that returns a reducer function. In this case, the reducer function is defined inside the Reduce block.
6. Reduce { state, action in }
The Reduce block is where we handle how the app’s state should change in response to different actions. It takes two parameters:
• state: The current state of the app.
• action: The action that has been dispatched (e.g., increment, decrement).
The switch action statement allows you to handle different actions and define how each one should modify the state.
Next, open the TickerApp.swift file. Since we removed ContentView.swift, the app likely won’t compile anymore.
To fix this, start by importing ComposableArchitecture. Then, within the WindowGroup, replace ContentView() with HomeView(store:) to properly connect your new view to the app’s structure. This will ensure the app compiles and links to the HomeView we set up earlier.
In this part of the project, we should first connect the HomeView to its HomeStore by passing in a store. Since we’ve already covered what the store and reducer, let’s focus on why we need to pass them and how it ties everything together.
WindowGroup
This part is standard in SwiftUI and sets up the main window of the app. It ensures that HomeView is displayed when the app launches.
2. HomeView(store: .init(…))
In this line, we’re initializing and passing the store to HomeView. Let’s break down why this is important:
- initialState: We define the app’s starting state here by passing an instance of HomeStore.State(). This could include any initial values or configurations your app needs when it starts. For now, it’s an empty state, but as we build out the app, this will hold things like the ticker count.
- reducer: The reducer handles how the app responds to actions and updates the state. By passing in HomeStore(), we give the store the ability to process actions and modify the state as needed.
Now that you’ve got a basic understanding of The Composable Architecture (TCA), it’s time to start building our Ticker App further.
We are going to keep it simple, we’ll display a count using a Text element and use two buttons — one to increment and one to decrement the count. However, there’s a small twist: if the counter reaches zero, the user won’t be able to decrement it further, preventing negative values.
The idea for this app was inspired by the Point-Free team. I initially struggled to come up with something original, but I realized that adding complexity would just distract us from learning about visionOS and the core principles of TCA.
Next, let’s explore window styles. Place your cursor right after the closing curly brace of the WindowGroup and type .windowStyle. As of the date of writing, Apple allows you to choose from three options: .automatic, .plain, and .volumetric.
Here’s a breakdown of each:
• .automatic: This adds a glass background to your window.
• .plain: A plain window does not include a glass background. Use this style when you want more control over the appearance of your window elements.
• .volumetric: This style is ideal for displaying 3D content within a defined space, also known as a volumetric window or “volume.”
Since we don’t want a glass background for our window, we’ll select .plain, allowing us full flexibility in designing the window.
> Setup HomeStore
Next, navigate to the ViewStore.swift file, where we’ll define the state properties and actions for our view.
Inside the State structure, add the following property:
This will track the state of our counter. On the next line, add:
This property will determine whether the decrement button is enabled or disabled.
Your final State struct should look something like this:
Now, let’s define the actions our app can take. Inside the Action enum, add the following cases:
Once you’ve added these cases, Xcode will likely display an error in the Reducer body:
Missing return in closure, expected to return ‘Effect<HomeStore.Action>
This error indicates that you need to handle side effects for the new cases in your Action enum. To resolve this, add case .increment and case .decrement to your switch statement and remove the default case, as it’s no longer needed
At this point, Xcode might show two additional errors. These occur because we haven’t returned any side effects from each case. Since we don’t expect any side effects, simply return .none for both case .increment and case .decrement.
Let’s recap what we expect our app to do:
• Display a Text view showing the current count.
• Provide two buttons: one to increment the count and another to decrement it.
• Disable the decrement button when the count reaches 0, preventing negative values.
We’ve already set up the state properties and corresponding action cases. Now, let’s handle incrementing the count.
Inside the increment case of the action switch statement, before returning the side effect, we’ll increase the count by calling the count property from our state object, captured in the Reducer:
After incrementing the count, we’ll check if decrementDisabled is true. If it is, we’ll toggle it to false, since the count is now greater than zero:
For the decrement case, we’ll do almost the opposite. First, we’ll decrease the count:
Then, we’ll check if the count has reached zero. If so, we’ll set decrementDisabled to true to disable the decrement button:
You Reducer must look someting like this:
Great work, we’ve finished setting up HomeStore, let’s move on to the HomeView.swift file.
> Setup HomeView
Assuming you’re already familiar with creating basic SwiftUI views, I’ll skip the setup and jump straight to the important parts. Please copy and paste the following code inside the body of HomeView, replacing any boilerplate code. I’ve included some TODOs for the most crucial parts of this blog.
Handling the Decrement Event
Let’s start by handling the decrement action. Replace TODO: 1 with:
This is where the magic of TCA comes into play. You’re sending the .decrement event to the store, and the reducer applies the changes to the state, causing the view to re-render. Similarly, for the increment action, replace TODO: 2 with:
Disabling the Decrement Button
Next, add the .disabled modifier to the minus button by accessing decrementDisabled from the store, replace TODO: 3 with:
Enhancing the UI
Now that the functionality is complete, let’s enhance the UI. Replace both TODO 4 and 5 with .glassBackgroundEffect(). Remember, we chose .plain for our window style at the beginning, so our window has no background at all, this modifier adds a 3D glass effect to your UI elements, including thickness, blur, shadows, and more.
Here is the final code for the HomeView:
> Final Step
Go ahead and build and run the project.
Congratulations on creating (probably) your first visionOS app using the TCA framework! In this blog, you’ve learned the basics of TCA, which we’ll continue to leverage throughout this visionOS blog series. You’ve also gained valuable insights into window styles and how to apply glass effects to your UI elements in visionOS.