Reading Time: 6 minutes

 

Welcome back! If you’ve been following along with this blog series, it’s great to have you here again. If this is your first time joining, I highly recommend starting with the earlier posts: A Practical Guide to visionOS Development using TCA (Part 1) and A Practical Guide to visionOS Development using TCA (Part 2: Handling Immersive Spaces).

In this blog, we’ll focus on managing multiple windows in a visionOS application. Specifically, we’ll explore how to open and close windows seamlessly to enhance user interaction. As before, we’ll be working with the provided starter projects. This tutorial assumes you’re already familiar with setting up a visionOS project, integrating necessary swift packages, and building basic SwiftUI elements. If you’re not, I recommend reviewing the first two blogs before diving in.

Months App:

The app will display the 12 months of the year in a collection-style list. When a user selects a month, a new window opens, showing information about the origin of the month’s name and a fun fact related to it.

TODO: #1 And TODO: #2

Open the MainView.swift file. We’ll replace Todos 1 and 2 with two crucial environmental actions which are the key focus of this tutorial, openWindow and dismissWindow, which manage the entire flow of presenting and dismissing windows in visionOS.

Copy to Clipboard

TODO: #3

Open the MainFeature.swift file and, within the State, add a property to track the currently active windows.

Note: In the previous blogs, we referred to stores as ViewStore, MainStore, etc. However, since the majority of the TCA community uses “Feature” for naming stores, I’ve decided to adopt this convention for the remaining blogs. While naming your stores is purely a matter of personal preference, aligning with industry and community standards is highly recommended for consistency and clarity.

Copy to Clipboard

 

We are using a Set for the openWindows property because a Set ensures that each window (represented by a MonthType) is unique. This prevents duplicate entries, which is important when tracking active windows. Additionally, Set provides efficient operations for adding, removing, and checking the presence of elements, making it a suitable choice for managing the state of open windows.

TODO: #4, #5 And #6

We will add the following cases to the Action enum:

Copy to Clipboard

 

Why We’re Adding These Cases

These actions are necessary to manage the lifecycle of windows in the app. They enable us to handle the opening, closing, and state changes of individual windows dynamically. Each case plays a specific role in the overall functionality of window management:

openWindow(OpenWindowAction, MonthType): Handles the process of opening a new window for a specific month. The MonthType parameter indicates which month’s window should be opened.

dismissWindow(DismissWindowAction, MonthType): Manages the closing of a specific window. The MonthType ensures we dismiss the correct window.

case changeWindowState(for: MonthType): This action updates the openWindows set by either adding or removing the specified month, depending on whether it is currently present in the set.

For TODO: #7, we will exhaust the switch statement by adding the newly defined cases using the case let pattern:

Copy to Clipboard

 

This ensures the switch statement accounts for all defined actions, maintaining comprehensive and predictable handling of each case.

Using case let ensures we can destructure and access the associated values cleanly for each case.

Next, we’ll handle each case individually. Starting with the openWindow case, we’ll replace return .none with a runeffect. Within this effect, we’ll capture send to dispatch the .changeWindowState action, passing the selected month. On the next line, we’ll call the action property (an OpenWindowAction) and pass the month’s rawValue. This environmental action will then open the corresponding window. Note that for now, the only window in the app is the main app window, but this setup prepares us for managing additional windows in the future.

Copy to Clipboard

 

The provided code snippet introduces a run effect for the .openWindow case, enabling asynchronous behavior when the action is triggered.

.run Effect:
The .run effect allows you to perform asynchronous work and interact with the store by sending additional actions. This is particularly useful for handling side effects.

await send(.changeWindowState(for: month)):
This line sends the .changeWindowState(for: month) action to the reducer. It updates the state by adding or removing the given month from the openWindows set, ensuring that the app tracks the window’s state accurately.

await action(id: month.rawValue):
The action here refers to the OpenWindowAction environmental action. It uses the rawValue of the selected month to identify the corresponding window and open it. This is the mechanism that triggers the actual window opening process.

Next, we’ll follow a similar approach for the dismissWindow action.

Copy to Clipboard

 

Lastly, we’ll handle the changeWindowState case. Since this action doesn’t involve any asynchronous behavior, we’ll simply return .none, indicating no side effects. This case is responsible for keeping track of the currently active windows.

Copy to Clipboard

 

Purpose: This code toggles the state of a specific month in the openWindows set.

Logic: If the month is already in openWindows, it’s removed (closing the window). If it’s not present, it’s added (opening the window).

Why return .none? Since this is purely a state update with no external dependencies or asynchronous tasks, there’s no need to trigger any side effects.

TODO: #8

Let’s return to the MainView.swift file. Here, we’ll implement logic to handle opening or dismissing windows based on the current state. Using the following code snippet:

Copy to Clipboard

 

Condition Check: The code checks whether the month.type is already present in the openWindows set.

If it is, the .dismissWindow action is dispatched to close the corresponding window. If it isn’t, the .openWindow action is dispatched to open a new window for that month.

Dynamic Behavior: This ensures that selecting a month either opens a new window or closes an existing one, based on the current state.
This dynamic approach keeps the user interface in sync with the app’s window state.

TODO: #9

Next, navigate to MonthsApp.swift, where we’ll add multiple WindowGroup instances, each assigned a unique identifier.
By assigning a unique identifier to each WindowGroup, the app can distinguish which window to open or dismiss when the environmental OpenWindowAction or DismissWindowAction is triggered. In our app, we use the MonthType’s rawValue as the window ID, allowing the app to precisely manage the corresponding windows dynamically. This ensures that the correct window is opened or closed based on the actions.

Copy to Clipboard

TODO: #10

Lastly, navigate to MonthView.swift, where we’ll trigger the .getMonthInfo action upon the appearance of the MonthViewto load the relevant information for the selected month.

Copy to Clipboard

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

Summary:

In this blog, we built functionality to manage multiple windows dynamically in a visionOS app. We added actions to open, dismiss, and track windows, updated the reducer to handle these actions, and used WindowGroup with unique identifiers for precise control over each window. Finally, we set up MonthView to load relevant information for the selected month upon view appearance.

Note: It’s important to note that visionOS determines the placement of new windows in the user’s field of view. While our app manages the logic for opening and dismissing windows, the system ultimately decides where they appear.

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: December 23rd, 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