Streaming is available in most browsers,
and in the Developer app.
-
Work with windows in SwiftUI
Learn how to create great single and multi-window apps in visionOS, macOS, and iPadOS. Discover tools that let you programmatically open and close windows, adjust position and size, and even replace one window with another. We'll also explore design principles for windows that help people use your app within their workflows.
Chapters
- 0:00 - Introduction
- 1:19 - Fundamentals
- 6:15 - Placement
- 9:43 - Sizing
- 12:03 - Next steps
Resources
Related Videos
WWDC24
WWDC23
-
Download
Hi everyone! I’m Andrew, and I work on SwiftUI. I’m super happy to talk to you about working with windows in your SwiftUI app! Windows are a container for the content of your app. A window allows people to manage parts of your app with familiar controls. Like being able to reposition, resize, or close it. I’ll be working on BOT-anist, a SwiftUI app my friends and I have been working on.
Here in the simulator, is the BOT-anist robot editor, where a robot can be customized. Players can bring this robot into a game where they help the robot tend to plants.
BOT-anist has a tailored experience for iOS, iPadOS, visionOS, and macOS.
The concepts I’ll discuss apply to multi-window platforms. But, in this video, I’ll focus on visionOS. I’ll talk about how to define, open, and use a window. I’ll cover how to control the initial placement of windows, and I’ll discuss the different ways, windows can be sized. First up, the fundamentals.
With individual windows people can use different parts of your app at the same time. And, having multiple instances of the same interface can be really powerful.
People can use system controls to independently manipulate each window. Like being able to resize, reposition, or scale it.
And, each window can take advantage of platform specific features. For example, on visionOS, a window can contain 3D content by using a volumetric window style. While multiple windows are powerful, using a single top-level view, like a TabView, can simplify the experience.
To learn more about TabView and other top-level views, check out “Elevate your windowed app for spatial computing” To learn when multiple windows are appropriate in visionOS, check out “Design for spatial user interfaces”.
BOT-anist, has two primary scenes in visionOS: The editor window and the game volume.
Each scene is defined by a WindowGroup. The app opens to an instance of the robot editor WindowGroup. And a button in this window, opens an instance of the “game” WindowGroup. The volumetric window style makes this window a volume in visionOS. I’d like to add two new features to BOT-anist.
The first, will open a new window containing a movie made about the robot. The movie will be a 3D scene contained in a portal.
In the app body, I add a new WindowGroup that contains the 3D scene view. To identify this WindowGroup, I’ve given it an ID of “movie". I’ll use this ID to open the window. I’ll pass the ID to an Environment Action. These actions are available at any point in the SwiftUI hierarchy. A few different environment actions are available for managing windows.
Use openWindow to open a window. To close a window use dismissWindow.
pushWindow can be used to open a window and hide the originating window.
I’ll use openWindow to open a new movie window. In the robot editor view, I retrieve an OpenWindowAction from the environment by creating an environment property with a key path of openWindow. And then, within a new button, I can perform the OpenWindowAction, passing in the ID I defined for the window group earlier… “movie”.
Now, tapping the button in the editor opens the Movie portal as a separate window.
Now that I see it, I don’t think the editor should be visible at the same time as the movie view. So I’ll use the pushWindow environment action to present the window instead.
This will open the new window in place of the originating window. Closing the new window will result in the originating window reappearing.
To hide the editor when opening the movie window, I change the environment property key path from openWindow to pushWindow, and update the button to call this action instead.
Now, tapping the TV button will push the movie window and hide the robot editor window. Now I can watch the robot I’ve designed begin their acting career without any distractions.
Tapping the close button will take me back to the editor. No additional logic is needed to get this behavior. Consider using this action when showing content that doesn’t need to be visible at same time as the presenting window. With the windows defined and opened, they can now be enhanced to feel even more at home with platform specific features: like how Freeform uses a toolbar ornament to display controls along the bottom edge of a window, or how a ToolbarTitleMenu presents actions related to a document without crowding the canvas; the window bar and close button are always visible by default. But, for the movie view, I used the .persistentSystemOverlays modifier to hide these, to let people focus on the movie.
These APIs are some great ways to enhance a window in visionOS. For refining a window in macOS, check out “Tailor macOS windows with SwiftUI”. The movie window is looking great! Next, I’d like to add an optional control panel for the game. This panel will have additional controls for moving the robot and a few buttons to perform actions like jump or wave. I’ve added a new window group that displays the controls.
And also an openWindow call in the game volume.
Now, tapping the button in the game opens my controls in a new window. I like that they can repositioned independently of the game volume.
But, when the window first opens, it covers the volume and might be positioned far away. visionOS places new windows, like the control panel, in front of the originating window. macOS on the other hand, opens new windows at the center of the screen. This behavior can be customized with the defaultWindowPlacement modifier. It allows the initial position and size of a window to be set programmatically. Depending on the platform, windows can be positioned and sized in a few ways. They can be positioned relative to other windows like a leading or trailing position: relative to people, with a position like utilityPanel in visionOS, which places the window close by and generally within direct touch range; or, relative to the screen, like the top right quadrant in macOS.
To make the game controls appear close to the player in visionOS, I apply the defaultWindowPlacement modifier to the “controller” window group.
From this, I return a WindowPlacement with a position of .utilityPanel.
I wrap this return, in an if condition so that this placement is only applied for visionOS.
Now the controls appear close by when the window is first opened. And the player is able to move the window from its initial placement if they like.
Using these new controls I’m able to interact with the robot in a whole new way! Like tapping this button, to make the BOT-anist wave! The controller window is looking great in visionOS! Next, in macOS, I’ll calculate a position for this window manually. The defaultWindowPlacement modifier provides a context. Depending on the platform, this will contain different information. In macOS, the context contains information about the default display. I access that and get the .visibleRect. This represents where it is safe to place content.
Using the sizeThatFits method, I ask the contents of the window, what size they would like to be. Using the displayBounds and size variables, I calculate a position that’s just above the bottom of the display and centered horizontally.
Now, I can return a WindowPlacement with the calculated position and the size.
Now my controls are positioned comfortably on macOS as well. While playing, the player is free to reposition the window, or even place it on a separate screen. I’m loving these new window placements! To make sure my content is always looking its best, I’d like to change how the window can be resized as well. Windows have an initial size determined by the system. You can change the default size in a few different ways.
If the size depends on the screen size or other windows, you can specify an initial size through the default window placement API, like I did for the controller window in macOS. Alternatively, you can use the defaultSize modifier to change the initial size. Note that this default size, is not used if there are other size constraints, like a size provided by the window placement API or when scenes are restored.
For a pushed window, like the movie window I added earlier, the defaultSize will be the same as the originating window’s size. The originating window in this case, is the robot editor. I’m happy with the default size, but players may want to resize the movie window. I’ll set some limits, so the movie always looks good.
By specifying that the "movie" WindowGroup should have a .windowResizability of .contentSize, the window will be limited to the min and max size of the content it contains. To the movie content view, I add a min and maxWidth, and a min and maxHeight.
Now the movie window can be resized down to a square and resized up within reasonable limits.
I could watch the BOT-anist all day! But, I should really focus on the controls window.
It can be resized to be too large, getting in the way of the volume. It makes sense for the size of this window to match the size of the content it contains.
Just like I did for the movie WindowGroup, I also add a windowResizability modifier to the controller window group.
Now, when I change the controller mode, the window resizes to match the size of the content.
Note that this window is not resizable by the player, because the views for each mode have fixed sizes, not min and max sizes.
BOT-anist is coming along really well! I’ve made some great improvements to the app for visionOS & macOS. Your app, can also make great use of windows and the API’s that support them.
Consider whether a window or a top-level view makes the most sense for your app. Use the window placement API to provide an initial layout. Size windows based on their content and set limits on how a window can be resized. And make use of platform specific window features, to make your app feel even more at home.
Thanks for joining me! I hope you enjoy working with windows in your app.
-
-
2:36 - BOT-anist scenes
@main struct BOTanistApp: App { var body: some Scene { WindowGroup(id: "editor") { EditorContentView() } WindowGroup(id: "game") { GameContentView() } .windowStyle(.volumetric) } }
-
3:09 - Creating the movie WindowGroup
@main struct BOTanistApp: App { var body: some Scene { WindowGroup(id: "editor") { EditorContentView() } WindowGroup(id: "game") { GameContentView() } .windowStyle(.volumetric) WindowGroup(id: "movie") { MovieContentView() } } }
-
3:55 - Opening a movie window
struct EditorContentView: View { @Environment(\.openWindow) private var openWindow var body: some View { Button("Open Movie", systemImage: "tv") { openWindow(id: "movie") } } }
-
4:45 - Pushing a movie window
struct EditorContentView: View { @Environment(\.pushWindow) private var pushWindow var body: some View { Button("Open Movie", systemImage: "tv") { pushWindow(id: "movie") } } }
-
5:34 - Toolbar
CanvasView() .toolbar { ToolbarItem { Button(...) } ... }
-
5:40 - Title menu
CanvasView() .toolbar { ToolbarTitleMenu { Button(...) } ... }
-
5:48 - Hiding window controls
WindowGroup(id: "movie") { ... } .persistentSystemOverlays(.hidden)
-
6:28 - Creating the controller window
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() } WindowGroup(id: "controller") { ControllerContentView() } } }
-
6:34 - Opening the controller window
struct GameContentView: View { @Environment(\.openWindow) private var openWindow var body: some View { ... Button("Open Controller", systemImage: "gamecontroller.fill") { openWindow(id: "controller") } } }
-
7:46 - Positioning the controller window
WindowGroup(id: "controller") { ControllerContentView() } .defaultWindowPlacement { content, context in #if os(visionOS) return WindowPlacement(.utilityPanel) #elseif os(macOS) ... #endif }
-
8:45 - Positioning the controller window continued
WindowGroup(id: "controller") { ControllerContentView() } .defaultWindowPlacement { content, context in #if os(visionOS) return WindowPlacement(.utilityPanel) #elseif os(macOS) let displayBounds = context.defaultDisplay.visibleRect let size = content.sizeThatFits(.unspecified) let position = CGPoint( x: displayBounds.midX - (size.width / 2), y: displayBounds.maxY - size.height - 20 ) return WindowPlacement(position, size: size) #endif }
-
10:12 - Default size
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() } .defaultSize(width: 1166, height: 680) } }
-
10:49 - Setting resize limits on the movie window
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() .frame( minWidth: 680, maxWidth: 2720, minHeight: 680, maxHeight: 1020 ) } .windowResizability(.contentSize) } }
-
11:37 - Controller window resizability
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "controller") { ControllerContentView() } .windowResizability(.contentSize) } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.