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:

  1. .mixed: Maintains a connection with the real world.
  2. .progressive: Offers variable immersion levels, adjustable via the digital crown.
  3. .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!