In the ever-evolving world of iOS development, SwiftUI stands as a beacon of modern UI framework design, offering simplicity, efficiency, and a declarative syntax. As a Swift developer, but really an Objective-C developer at heart, my first real dive into SwiftUI while developing Orbit for Apple Vision Pro was exhilarating, while at times incredibly frustrating! This journey uncovered valuable lessons and techniques that reshaped my approach to iOS development.
In this post, I'll share seven code snippets and lessons I learned from my experience with SwiftUI and the APIs available in visionOS. Whether you're new to SwiftUI or looking to deepen your understanding, I hope that these insights provide valuable takeaways to enhance your development skills.
1. How to use NavigationStack
and NavigationPath
in SwiftUI
In SwiftUI, navigation paradigms have evolved significantly. One of the most notable additions is NavigationStack
. Prior to iOS 16, NavigationView
and NavigationLink
were the go-to for creating hierarchies and navigation paths. However, NavigationStack
combined with NavigationPath
offers a more intuitive and flexible way to manage navigation hierarchies.
@State var navigationState: NavigationPath = .init()
var body: some View {
NavigationStack(path: $navigationState) {
MenuView(navigationState: $navigationState)
.navigationDestination(for: OnboardingStage.self) { stage in
switch stage {
case .emotion:
EmotionSelectionView(navigationState: $navigationState)
case .environment:
EnvironmentSelectionView(navigationState: $navigationState)
case .experience:
ExperienceMeditationView(navigationState: $navigationState)
}
}
}
}
...
Button(action: {
navigationState.append(OnboardingStage.environment)
})
...
In this example, NavigationStack
effectively manages the navigation flow from the various stages of onboarding in Orbit. The .navigationDestination
modifier ensures that the correct view is displayed via the switch-case whenever the navigationState
is appended.
2. How to use the new Observable
macro in SwiftUI
Starting with iOS 17, SwiftUI introduced the Observable
macro, a game-changer in how we handle state management. This macro is a more efficient alternative to the ObservableObject
protocol, particularly advantageous in tracking optionals, collections, and leveraging existing data flow primitives like State
and Environment
.
@Observable
class MeditationPreferences {
var selectedTypes: [MeditationType] = [.guided, .freeform]
}
enum MeditationType: String {
case guided, freeform, music
}
struct MeditationPreferenceView: View {
@State private var preferences = MeditationPreferences()
var body: some View {
List(preferences.selectedTypes) { meditationType in
print(String(describing: meditationType))
}
}
}
In this code snippet, MeditationPreferences
is marked with @Observable
, allowing its consumers to respond to changes in its properties – including changes to its collections. The MeditationPreferenceView
can then use this @Observable
to provide a dynamic and interactive interface based on changes to selectedTypes
.
3. Using @Environment
values for opening and dismissing immersive spaces
SwiftUI's @Environment
is required for toggling between immersive and non-immersive states in visionOS. The introduction of .openImmersiveSpace
and .dismissImmersiveSpace
offers a seamless way to manage these states, crucial for apps wanting to add a three-dimensional layer of depth and immersion. It didn't immediately click for me, but these environment values are simply callable functions.
struct MeditationSpace: View {
@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace
var body: some View {
VStack {
Text("Welcome to your meditation space")
.padding()
Button("Start session") {
Task {
await openImmersiveSpace(value: "MeditationSpace")
}
}
Button("End session") {
Task {
await dismissImmersiveSpace()
}
}
}
}
}
...
var body: some Scene {
ImmersiveSpace(id: "MeditationSpace") {
ImmersiveView()
}.immersionStyle(selection: .progressive, in: .progressive, .full)
}
...
In this snippet, MeditationSpace
utilizes the .openImmersiveSpace
and .dismissImmersiveSpace
values to control the state of the immersive environment. By tapping "Start session", the user enters an immersive space tailored to the selection
immersion style (i.e. .progressive
). The "End session" button allows for a smooth exit from the immersive experience.
4. How to use the .ornament
modifier with SwiftUI Views
SwiftUI's .ornament
modifier opens up creative avenues for adding decorative elements to views in three-dimensional space. The ornament
modifier allows you to specify the visibility, attachment anchor, content alignment, and the ornament view itself.
struct MeditationSessionView: View {
var session: MeditationSession
var body: some View {
Text("Meditation Session: \(session.type)")
.ornament(attachmentAnchor: .scene(.bottom)) {
CalmingOrnamentView()
}
}
}
In this example, CalmingOrnamentView
is used as an ornament at the bottom of the MeditationSessionView
. This adds a visually pleasing element to the session view, augmenting the user's three-dimensional space.
Big thanks to Jordi Bruin for putting together such great examples in his visionOS-Examples project!
5. How to use the .glassBackgroundEffect
modifier for blurred backgrounds in visionOS
The .glassBackgroundEffect
modifier in SwiftUI is a powerful tool for adding depth and texture to your app's interface. It's especially effective in creating immersive experiences, where the environment plays a key role in user engagement.
This modifier adds a glass-like material effect that includes depth, blur, and shadow features, contributing to a 3D appearance. The key to effectively using this effect lies in applying it to a ZStack
or as a background to ensure it renders properly in the layout.
struct MeditationEnvironmentView: View {
var body: some View {
ZStack {
MeditationContentView()
}
.glassBackgroundEffect(
in: RoundedRectangle(
cornerRadius: 46,
style: .continuous
)
)
}
}
In this code, the MeditationEnvironmentView
utilizes the glassBackgroundEffect
to provide a visually rich, glass-like background.
6. What are the different Immersion Styles in visionOS
The visionOS API offers three distinct immersion styles that cater to different levels of user engagement. These styles are particularly relevant in apps like Orbit, where the user's experience can range from partially immersive to fully encompassing.
The three styles are:
.mixed
: Maintains a connection with the real world..progressive
: Offers variable immersion levels, adjustable via the digital crown..full
: Completely immersive, obscuring the real-world environment.
The .immersionStyle
modifier allows you to set the initial style, along with the set of styles that are available for a particular immersive space.
struct MeditationSpaceScene: Scene {
@State private var immersionStyle: ImmersionStyle = .progressive
var body: some Scene {
ImmersiveSpace(id: "MeditationSpace") {
MeditationEnvironmentView()
}
.immersionStyle(selection: $immersionStyle, in: .progressive, .full)
}
}
struct MeditationEnvironmentView: View {
var body: some View {
Text("Welcome to Your Meditation Space")
}
}
Here we've defined our MeditationSpaceScene
which registers the ImmersiveSpace
with ID MeditationSpace
as a space that can later be opened via the openImmersiveSpace
environment value as described above.
7. How to add Disclaimer Text to a Section
in SwiftUI
In SwiftUI, adding disclaimers or informational text at the bottom of a section can significantly improve user understanding, especially for features that involve personalization or data use. SwiftUI's Section
view allows for adding footer text, which can be used to convey important information, disclaimers, or guidance to users in a clear and standardized way.
struct PersonalizationSettingsView: View {
@State private var firstName: String = ""
@State private var toggleState: Bool = false
private var footerText: String = "By enabling personalization..."
var body: some View {
List {
Section(footer: Text(footerText)) {
HStack(spacing: 12) {
SymbolView(symbolName: "person.fill")
TextField("Enter your name", text: $firstName)
}
HStack(spacing: 12) {
SymbolView(symbolName: "hand.raised.fill")
Toggle(isOn: $toggleState) {
Text("Allow personalization")
}
}
}
}
.listStyle(GroupedListStyle())
}
}
In this example, the Section
includes a footer property in its initializer that perfectly handles placement of the Text
beneath the section.
Huge thanks to Jordan Morgan for helping me find this functionality within Section
!
Embarking on the SwiftUI journey with the development of Orbit for Apple Vision Pro has been a pathway of discovery. These 7 APIs in SwiftUI and visionOS not only enhance Orbit but also broadened my understanding of SwiftUI's capabilities. As visionOS continues to evolve, so will the opportunities to create more intuitive, engaging, and visually stunning apps. I hope these lessons from my experience will inspire and guide you in your SwiftUI endeavors. Have questions or feedback? Find my on Twitter @bryandubno. Happy coding!