visionOS basics for iOS developers: a step-by-step guide
Apple Vision Pro represents a significant shift in how we think about app interfaces, but the good news is that your existing SwiftUI knowledge translates remarkably well. If you've built iOS apps, you already understand the core primitives—views, state management, and layouts—that power visionOS. The learning curve isn't steep; it's more about understanding three key concepts: how windows work, what volumes and immersive spaces mean, and which interaction patterns feel natural in three dimensions.
SwiftUI apps run in windows
When you run a SwiftUI app on visionOS, it doesn't fill the user's entire field of view like an iPad app would. Instead, your interface appears in a floating window that the user can move, resize, and position anywhere in their physical space. This is fundamentally different from iOS development where you control the entire screen.
Your existing SwiftUI code actually runs with minimal changes. A basic List or Form will work exactly as you'd expect—the operating system handles rendering it at the correct depth and perspective. You don't need special 3D knowledge. What you do need to understand is that windows have default sizes and you should design your layout to work well when users move and resize them.
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationStack {
List {
ForEach(items, id: \.id) { item in
NavigationLink(value: item) {
Text(item.title)
}
}
}
.navigationTitle("My App")
}
}
}This code runs on visionOS without modification. The system automatically renders your List with appropriate depth and perspective. However, you should test how your layout responds when windows are smaller or larger than you expect. Consider using WindowGroup modifiers to set default sizes:
WindowGroup {
ContentView()
}
.windowStyle(.automatic)
.defaultSize(width: 600, height: 800)The key insight: your window is a viewport into a 3D space. Users can look around it, move closer or farther away, and resize it. Design assuming your content might not fill the entire screen.
Volumes and immersive spaces
While windows are traditional floating panels, volumes and immersive spaces let you create more spatial experiences. A volume is a bounded 3D container—think of it as a window that can show content at different depths. An immersive space replaces the user's surroundings entirely, expanding your app to fill their entire vision.
Most iOS developers migrating to visionOS should start with windows, but understanding volumes helps you plan for future features. A volume might show a 3D model viewer where you can see depth relationships between objects. An immersive space might be used for a game or architectural visualization where you want full environmental presence.
Here's how you add a volume to your app:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
Volume {
ModelViewerScene()
}
}
}And an immersive space:
@main
struct MyApp: App {
@State private var showImmersive = false
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.openImmersiveSpace) { id in
Task {
showImmersive = true
}
}
}
ImmersiveSpace(id: "explorer") {
ImmersiveExplorerView()
}
.immersionStyle(selection: .constant(.progressive), in: .progressive)
}
}For most production visionOS apps built by teams migrating from iOS, windows are your bread and butter. Volumes come next as you grow confident with spatial layouts. Immersive spaces are for specialized experiences where you need total environmental control. Start with windows, measure the user feedback, then expand.
Hover effects and eye-friendly targets
On visionOS, users don't have a mouse pointer—they use their eyes to look at content and pinch their fingers to select. This completely changes how you think about interactive targets. Buttons and tappable areas need to be larger than they are on iPhone, and they need clear hover feedback so users know what they're looking at.
When a user gazes at a button, you should provide immediate visual feedback. On iOS, you might use a subtle scale or color change on tap. On visionOS, you need that feedback the moment their gaze enters the button, before they pinch. This is the key interaction pattern difference.
struct MyButtonView: View {
@State private var isHovered = false
var body: some View {
Button(action: {
performAction()
}) {
Text("Press me")
.font(.headline)
.frame(width: 200, height: 60)
.background(isHovered ? Color.blue : Color.gray)
.foregroundColor(.white)
.cornerRadius(12)
}
.onHover { hovering in
isHovered = hovering
}
}
}The onHover modifier is your primary tool for visionOS interaction design. Unlike iOS where hover doesn't exist on touch devices, visionOS users spend most of their time gazing at things before acting. Use this to your advantage—make hover states prominent and make targets large.
Apple recommends minimum touch targets of 60 points on a side for visionOS. This accounts for the precision of eye tracking and the natural variance in how users control their gaze. Buttons that work fine on iPhone—tiny 44×44 squares—feel uncomfortable on visionOS.
VStack(spacing: 20) {
Button("Option A") { selectOptionA() }
.frame(height: 60)
.frame(maxWidth: .infinity)
Button("Option B") { selectOptionB() }
.frame(height: 60)
.frame(maxWidth: .infinity)
}
.padding()Also consider that visionOS users might be wearing the device for extended periods. Avoid interactions that require rapid repeated gestures or high precision. If your iOS app has complex gesture sequences, simplify them for visionOS or offer an alternative interaction method.
Start with your iPad app
The most practical path to visionOS is to take your existing iOS app—particularly if you have an iPad version—and run it unchanged. visionOS can run iOS and iPadOS apps in compatibility mode. Your app will appear in a window, and most gestures and interactions will work.
After testing your app running unmodified, you have three options: ship it as-is (quick path to visionOS), make small visionOS-specific tweaks (optimize for the platform without major rewrites), or create a full visionOS experience (new design and features).
We recommend the middle path for most teams. Your core logic—data fetching, business rules, state management—doesn't change. What changes is the presentation layer. Adjust your layouts for floating windows, add hover states to interactive elements, and test on-device. You're not rewriting your app; you're adapting its interface.
import SwiftUI
struct ContentView: View {
@State private var selectedItem: Item?
@State private var isHovered: Set = []
var body: some View {
NavigationStack {
List(items, id: \.id) { item in
NavigationLink(value: item) {
VStack(alignment: .leading, spacing: 8) {
Text(item.title)
.font(.headline)
Text(item.description)
.font(.caption)
.foregroundColor(.secondary)
}
.frame(height: 70)
}
.onHover { hovering in
if hovering {
isHovered.insert(item.id)
} else {
isHovered.remove(item.id)
}
}
.background(isHovered.contains(item.id) ? Color.blue.opacity(0.1) : Color.clear)
}
.navigationTitle("Items")
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
}
}This approach—starting with iPad and incrementally visionOS-ifying it—lets you ship quickly while building the team's knowledge. You'll catch visionOS-specific issues early without betting the entire project on a platform-specific rewrite.
Common mistakes
- Ignoring window resizing: Many iOS developers assume fixed dimensions. visionOS users resize windows constantly. Test your layouts at multiple sizes, from 400×600 to 1200×1000 points.
- Tiny touch targets: A 44-point button that feels fine on iPhone creates eye strain on visionOS. Aim for at least 60 points minimum, preferably larger for frequently-used actions.
- No hover feedback: Shipping visionOS apps without hover states feels broken. Users expect immediate visual feedback when their gaze lands on interactive elements.
- Overcomplicating with 3D too early: RealityKit and SwiftUI 3D support are powerful, but they're not required for successful visionOS apps. Many successful production apps use traditional 2D layouts. Build the experience first, add 3D complexity later if it adds value.
- Forgetting about gesture differences: Pinch and long-press work differently on visionOS than iOS. Double-tap might not work as expected. Test all interaction patterns on-device.
Key takeaways
- SwiftUI apps run in floating windows on visionOS; your existing code works with minimal changes, but design for windows that users can move and resize.
- Volumes and immersive spaces are available for spatial experiences, but most production apps start with windows and expand later.
- Hover effects and eye-tracking mean you must provide visual feedback before the user pinches, and interactive targets should be at least 60 points on a side.
- The fastest path to visionOS is taking your iPad app, testing it unmodified, then making targeted UI adjustments for the new platform rather than a complete rewrite.
- Use
onHovermodifiers liberally, avoid tiny interaction targets, and test your layouts at multiple window sizes.






Comments
Loading comments…