Taking iOS apps to the next level on visionOS

8 May 2024

SwiftUI makes it easier than ever to develop apps for multiple Apple devices. In this blog post, I show you how.

When developing native iOS apps, you should aim to follow Apple’s human interface guidelines at all times. This has become much easier which the introduction of SwiftUI which provides a common user interface framework for all Apple devices.

By sticking to SwiftUI, you will be able to target multiple devices without having to create (much) device-specific UI logic. For example, iPhone code will mostly work on a Vision Pro. However, there will always be device-specific quirks that you will need to programme around. More on that later.

For this post, I am going to build upon the WWDC Wishlist app presented in a previous post that introduced SwiftUI and App Intents. The code for that app is available in Github.

Adding a native build target

When building a standard iOS application in Xcode 15, you will be presented with a list of 4 target platforms: iPhone, iPad, Mac (Designed for iPad), and Apple Vision (Designed for iPad). As the names suggest, the 'Mac (Designed for iPad)' and 'Apple Vision (Designed for iPad)' options build non-native iPad versions of the app that run on macOS and visionOS respectively.

Here’s how these options look in Xcode 15 when building our Wishlist app. We'll select the 'Apple Vision (Designed for iPad)' option.

Xcode 15 Supported Destinations screen

As can be seen, the generated application works on the Vision Pro, but it looks like an iPad app not a Vision Pro app. Let's fix that.

App Running on Apple Vision Pro

First, remove the 'Apple Vision (Designed for iPad)' target, then click the + button, select 'Apple Vision', and in the sub-menu, click 'Apple Vision' again.

Adding Target for Apple Vision in Xcode 15 Supported Destinations screen

Now let's run our app on the Apple Vision Pro again.

Running App on Vision Pro on Apple Vision Target

Et voila - the app now looks and feels like a native Vision Pro app. However, there is a problem: the Status buttons don’t look great.

The issue is that these buttons use colour and according to the Human Interface Guidelines for visionOS, we should “Use colour sparingly, especially on glass”. So, we need to provide a bespoke experience for visionOS.

Enter the compiler directive - a way to create platform-specific code. For example, the following directive will print "This is visionOS" when we run the visionOS target.

#if os(visionOS)
print("This is visionOS")           
#else
print("This is not visionOS")
#endif

Redesigning components to be native with visionOS UI

To fix the selector on visionOS, we are going to make use of SwiftUI's Picker control when running a visionOS version of the App.

var visionOSPicker: some View {
  Picker("Status", selection: $selectedItem.status) {
    ForEach(WislistItemStatus.allCases, id: \.self) { status in
      Text(status.title)
    }
  }
  .pickerStyle(.navigationLink)
}

Using a compiler directive, we can switch between the two selectors depending on the current type of App target.

Section("Status") {
  #if os(visionOS)
  visionOSPicker
  #else
  ipadOSPicker
  #endif
}

Now, let's run the app and checkout our improvements.

App Running on Vision Pro with Native SDK (Selector Closed)

App Running on Vision Pro with Native SDK (Selector Open)

Comparing the visionOS version of the app to the iOS SDK version, we see that:

  • visionOS uses glass material effect by default.

  • Buttons are slightly changed: A circle is added around the + button to make it easier for it to be selected through eye tracking.

  • Due to there not being a status bar in visionOS, the title is now at the top of the screen rather than below a status bar reserved space.

Before - Vision Pro (Using iOS SDK)

Using iOS SDK

After - Vision Pro (Using Apple Vision SDK)

Using Apple Vision SDK

Conclusion

When developing iOS apps, you should default to using SwiftUI for your UI code. Doing so will mean that you can maintain a single codebase no matter the number of platforms being targeted. Furthermore, through selective use of compiler directives, you can create platform-specific logic so that your apps maintain a native look and feel.

If you want to learn more, check out the Github project or (if you are looking for formal classroom training), our iOS course.

Article By
blog author

Jack Delaney

Software Engineer