A hands-free, immersive reader for Apple Vision Pro — turn pages with a look,

read anywhere from a forest to deep space.

A hands-free, immersive reader for Apple Vision Pro — turn pages with a look,

read anywhere from a forest to deep space.

ReadScape

ReadScape

THE VISION

When I got Apple Vision Pro, I tried using the native reading app. But the experience felt like a phone app that had simply been moved into a headset; the same interface, in a bigger space.


So I started asking a different question: what if reading were designed around what Vision Pro uniquely makes possible?


Two of its capabilities felt especially made for reading. First, you can interact just by looking; the most natural act there is. Second, it can change the space around you at will, so you're no longer stuck reading wherever you happen to be.


That's what Readspace is: to build the most immersive, most natural way to read.

DEVELOP TOOLS

DEVELOP TOOLS

  • Xcode(SwiftUI)

  • Reality Kit

  • ARKit(gaze/hand)

  • Apple Vision Pro

An immersive reading app for Apple Vision Pro, where you can read anywhere from a quiet forest to deep space. Designed and built independently

An immersive reading app for Apple Vision Pro, where you can read anywhere from a quiet forest to deep space. Designed and built independently

Core Experience, Part 1 Adaptive control ornament

Core Experience, Part 1 Adaptive control ornament

Core Experience, Part 1 Adaptive control ornament

Reading should be about the words, not the controls around them. So the main control in Readspace is designed to almost disappear.

By default, it's just a small circle floating at the bottom of the page. Look at it and tap, and it fluidly expands into a full control bar; tap an option, and a menu opens above it. Look away for a few seconds, or glance elsewhere, and it gently collapses back into a single circle.

This follows one of Apple's core principles for visionOS - deference: keeping the focus on content, and letting the interface step back.

Core Experience, Part 2 Turn the page with a look

Core Experience, Part 2 Turn the page with a look

Core Experience, Part 2 Turn the page with a look

Reading should feel effortless, so the primary way to turn a page is simply to look: gaze at the edge of the page and it advances on its own, no hands needed.

But gaze isn't always the right tool. Sometimes tracking isn't perfect; sometimes you just want deliberate control over the pace. So Readspace also supports manual page; turning by pinch and drag, always there as a way to take control whenever you want it.

Together, the two cover the full range of reading: gaze for immersive, hands-free flow, and touch for precision when you need it.

  1. Hands free - Eye Gaze Interaction

  1. Hands free - Eye Gaze Interaction

  1. Hands on - Pinch and Drag

  1. Hands on - Pinch and Drag

Core Experience, Part 3 Read anywhere

Core Experience, Part 3 Read anywhere

Core Experience, Part 3 Read anywhere

Readspace is designed to be read in different immersive environments: a quiet forest, a stream, deep space.

In the current version, these environments are set through visionOS's native Environments, with Readspace layered inside them. Bringing environment switching directly into the app so you can change your surroundings without leaving the reading experience is one of the next things I'm building.

Core Experience, Part 4 Comfort & Customize

Core Experience, Part 4 Comfort & Customize

Core Experience, Part 4 Comfort & Customize

Reading is personal, and so is comfort. Everyone's eyes and sense of space are different, so Readspace lets you adjust the window size and depth, the content size and font to whatever feels most natural to you reducing eye strain and letting you read comfortably for longer.

Challenge: Platform Limitations vs Design Trade-offs

Challenge: Platform Limitations vs Design Trade-offs

Challenge: Platform Limitations vs Design Trade-offs

My first idea for page scroll was simple: watch for when your eyes reach the last few words on the page, and turn it automatically. First instinct was using .onHover, since gaze-driven hover highlighting is very visible on visionOS. It even worked fine in the Simulator because the Simulator uses mouse position to simulate gaze.

On the actual device, it was a different story. .onHover simply doesn't fire from gaze alone. Digging into Apple's developer forums cleared it up: raw gaze coordinates are never exposed to third-party developers on visionOS, not even through the Enterprise APIs . It's a deliberate privacy decision; eye-tracking data can reveal a lot about attention and behavior, so Apple just doesn't open that door

Which means, on visionOS, gaze-based interaction really only has two paths:

1. The native "look + pinch" gesture: the system hands you the combined event, but never the raw "where you're looking" data itself.
2. Apple's purpose-built semantic APIs, like .scrollInputBehavior(.enabled, for: .look)

The final app ended up combining two complementary interaction methods:

1. Gaze at the top/bottom edge of the page triggers an auto page-turn built with .scrollInputBehavior(.enabled, for: .look), keeping the experience hands-free and immersive.
2. Pinch-and-drag as a manual page-turn added as a complement to the first, giving users precise control when they want a more deliberate pace, or don't want to rely entirely on gaze timing.

How I build Spec-driven development

How I build Spec-driven development

How I build Spec-driven development

  1. Architecture first

Before any code gets written, I write a detailed spec for each part of the appdefining its structure, its components, its constraints, and the rules it has to follow. Readspace runs on a clean MVVM architecture: a documented map of every file and its responsibility, native visionOS components rather than reinvented ones, a fully tokenized design system so nothing is hard coded, and each module designed to be extended, with explicit hooks for the features still to come.

# INDEX.md Readscape V1.0 Project Map


## Project Overview
- **App:** Readscape V1.0
- **Platform:** visionOS
- **Framework:** SwiftUI + visionOS native APIs
- **Entry Point:** Readscape_V1_0App.swift
- **Architecture:** MVVM ViewModels (@Observable) + SwiftData (Book, ReadingProgress)
- **ViewModels:** ReadingViewModel.swift, ToolbarViewModel.swift





## Current File Inventory


| File | Role | Status |
|———|———|————|
| SmartReader_V1_0App.swift | @main entry, WindowGroup + SwiftData container, injects ViewModels |  Stable |
| **ReadingViewModel.swift** | **@Observable ViewModel: book content, chapter loading, sidebar display logic, reading progress** |  Stable |
| **ToolbarViewModel.swift** | **@Observable ViewModel: toolbar expand/collapse state machine, submenu toggle, timer** | Stable |
| NavigationRootView.swift | Root view, TabView(.sidebarAdaptable) with Reading/Library/Settings |  Complete |
| NavigationModel.swift | NavigationTab enum (reading/library/settings) with icons |  Complete |
| ReadingView.swift | Reading tab: NavigationSplitView + title bar + toolbar ornaments |  Stable |
| ContentSidebar.swift | Sidebar pure view: chapter list, paragraph groups, progress gauge |  Stable |
| **ReadingToolbar.swift** | **Pure view: collapsible toolbar + submenu container (two-ornament architecture)** |  Complete |
| **ReadingEnums.swift** | **Business enums: ToolbarState, ActiveSubmenu, ReadingMode, EnvironmentMode, LayoutMode, ReadingFontSize, ReadingFont** |  Stable |
| ReadingTextView.swift | Paragraph scroll view, fade mask, scroll transition (typography modifiers dynamic) | 🔒 Frozen (partial unfreeze) |
| ReadingInteraction.swift | Gaze scroll + pinch-drag interaction | 🔒 Frozen |
| DesignSystem.swift | Design tokens (Typography, Layout, Color, Animation) + DSTextStyle modifier + ds* View extensions | 🔒 Frozen (additive only) |
| **SubMenuOption.swift** | **Reusable UI component: submenu option row with checkmark, title, subtitle, trailing icon** |  Stable |
| **ToolbarButton.swift** | **Reusable UI component: toolbar expanded-state button with icon + label** |  Stable |
| **SettingsCard.swift** | **Reading settings popover: FontSizeStepper + FontPickerGroup (pure view)** |  Stable |
| BookModel.swift | SwiftData models: Book + ReadingProgress | Stable |
| BookParser.swift | BookParser protocol + BookParserFactory | Stable |
| TXTParser.swift | TXT format parser: chapter splitting, paragraph extraction | Stable |
| ChapterContent.swift | ChapterContent + ChapterInfo data types | Stable |
| BookStorageManager.swift | File import/delete/resolve for book storage | Stable |
| LibraryView.swift | Library tab placeholder | 📝 Placeholder |
| SettingsView.swift | Settings tab placeholder | 📝 Placeholder |
| ImmersiveView.swift | Immersive space placeholder (commented out) | Paused |
| ToggleImmersiveSpaceButton.swift | Immersive space toggle (commented out) | Paused |


## Navigation Structure


App (NavigationRootView  TabView .sidebarAdaptable)
├── Reading (ReadingView.swift)
├── Mode
├── Standard Mode
├── Translation Mode
└── AI Extension Mode
├── Environment
├── Passthrough
├── Pine Forest
├── Sunset Beach
└── Minimalist Room
└── Layout
├── Landscape         
└── Portrait          
├── Library (LibraryView.swift — placeholder)
├── My Library
└── Bookstore
└── Settings (SettingsView.swift — placeholder)
    └── Typography & Display


## Spec Files


| Spec | Path | Covers | Status |
|———|———|————|————|
| FROZEN.md | specs/FROZEN.md | Locked components | Living document|
| INDEX.md | specs/INDEX.md | This file | Living document|
| **toolbar_Basic.md** | **specs/reading/toolbar_Basic.md** | **Complete** |
| **toolbar_Submenu.md** | **specs/reading/toolbar_Submenu.md** | **Complete** |
| **sidebar.md** | **specs/navigation/sidebar.md** |  **Complete** |
| **BookModel.md** | **specs/Database/BookModel.md** | **Complete** |
| **reading_SettingCard.md** | **specs/reading/reading_SettingCard.md** |**Complete** |
| standard_mode.md | specs/reading/mode/standard.md | **Complete** |
| translation_mode.md | specs/reading/mode/translation.md |  Not started |
| ai_extension.md | specs/reading/mode/ai_extension.md | Not started |
| layout.md | specs/reading/layout/ | **Complete** |
| environment.md | specs/reading/environment/ | Not started |
| library.md | specs/library/ | Not started |
| settings.md | specs/settings/typography.md | Not started |





## Design Rules (Quick Reference)


- All colors  `DesignSystem.Color.*`
- All fonts  `ds*()` view extensions
- All spacing  `DesignSystem.Layout.*`
- Backgrounds  `.glassBackgroundEffect()` or `DesignSystem.Color.backgroundMaterial`
- No hardcoded hex, RGB, or magic numbers anywhere





## Next Task
**Pending** 

  1. Directing the build

With those specs as the blueprint, I use an AI coding agent inside Xcode to move from specification to working implementation. My role is defining the architecture, making the technical decisions, diagnosing what goes wrong, and deciding how it gets solved.

A lot of the real work happens in the details deciding that two floating panels needed to be fully isolated so they wouldn't interfere with each other's layout, or tracking down why an animation was starting from the wrong origin.

toolbar_Basic.md
# Readscape V1.0  Reading Toolbar Spec


## Declaration
Use Apple Vision Pro native design patterns only.
All values reference `DesignSystem.Layout.*` / `DesignSystem.Animation.*` tokens.
Do NOT hardcode any numeric values.


## High-Level Requirement
Create a simple collapsible toolbar with 3 buttons: Mode/Environment/Layout.
- Follow native visionOS toolbar behavior like Control Center or Safari address bar.
- Phase 1: buttons display current selection only (submenu functionality reserved for Phase 2)

## Technical Constraints  
- Must NOT modify any FROZEN components

---

## 1. Native visionOS Style
- **Material:** `.glassBackgroundEffect(in: Capsule())` matches main window glass material
- **Position:** Attached to window bottom via native `.ornament(attachmentAnchor: .scene(.bottom))` no manual offset
- **Behavior:** Like native visionOS Control Center toolbar
- **Hover:** `.contentShape(.hoverEffect, Capsule())` + `.hoverEffect(.highlight)` let visionOS handle the visual effect while ensuring correct hover geometry

---


## 2. Component Structure


### Collapsed State (Default)

- **Shape:** Perfect circle, 68×68pt (`toolbarCollapsedWidth` × `toolbarCollapsedHeight`)
- **Background:** `.glassBackgroundEffect(in: Capsule())`  `Capsule()` on a square frame renders as a circle
- **Icon:** `circle.dotted` (size: `toolbarCollapsedIconSize` = 40pt)
- **Icon Container:** `toolbarCollapsedIconContainerWidth` × `toolbarCollapsedIconContainerHeight` (44×44pt)
- **Hover:** MUST remain circular  `Capsule()` on square frame guarantees this

### Expanded State
- **Shape:** Capsule (pill), height: `toolbarExpandedHeight` (68pt), width: auto-adaptive
- **Background:** `.glassBackgroundEffect(in: Capsule())`
- **Content:** HStack of 3 `ToolbarButton` components (reused from `DesignSystem.swift`)
- **Width:** Auto-adaptive via hidden `PreferenceKey` measurement view (NOT `.fixedSize()` on shell)

---


## 3. Button Configuration

| Button | Icon | Default Label |
|--------|------|---------------|
| Mode | `book.pages.fill` | "Standard" |
| Environment | `mountain.2.circle` | "Passthrough" |  
| Layout | dynamic via `LayoutMode.iconName` | "Landscape" |

- Layout icon switches between `rectangle.landscape.rotate` and `rectangle.portrait.rotate` based on `selectedLayout`
- Button spacing: `toolbarExpandedButtonSpacing` (8pt)
- Content padding: `toolbarExpandedContentPadding` (12pt)


## 4. States & Animations

### 4.1 Fluid Morph (Collapsed  Expanded)
- **Technique:** Single `Color.clear` container with animated frame width + `Capsule()` shape
  - Collapsed: `frame(width: toolbarCollapsedWidth, height: toolbarCollapsedHeight)` circle
  - Expanded: `frame(width: measuredExpandedWidth, height: toolbarExpandedHeight)` capsule
- **Spring animation:** `DesignSystem.Animation.standardSpring`
- **Content transition:** `.transition(.opacity)` icons/labels fade in during expand, fade out during collapse
- **Width measurement:** Hidden measurement view using `PreferenceKey` pre-calculates expanded width for smooth animation

### 4.2 Collapse Trigger
- **Manual Trigger:** User taps outside toolbar area (full-screen `Color.clear` tap target)
- **Auto Trigger:** User is NOT gazing at toolbar (`!isHovering`) AND `toolbarAutoCollapseTimeout` (3 seconds) elapses
- **Animation:** Reverse of expand animation (same spring)

---

## 5. Implementation Requirements

### 5.1 New File Creation
Create new file: `ReadingToolbar.swift`

### 5.2 Required Enums (in `ReadingEnums.swift`)

```swift
enum ToolbarState { case collapsed, expanded }

enum ReadingMode: String, CaseIterable, Identifiable {
    case standard = "Standard"
    case translation = "Translation"
    case `extension` = "Extension"
}

enum EnvironmentMode: String, CaseIterable, Identifiable {
    case passthrough = "Passthrough"
    case pineForest = "Pine Forest"
    case sunsetBeach = "Sunset Beach"
    case minimalistRoom = "Minimalist Room"
}

enum LayoutMode: String, CaseIterable, Identifiable {
    case landscape = "Landscape"
    case portrait = "Portrait"
    // Dynamic iconName: landscape → rectangle.landscape.rotate, portrait → rectangle.portrait.rotate
}
```

### 5.3 Additive Tokens (in `DesignSystem.swift`)

```swift
extension DesignSystem.Animation {
    static let toolbarAutoCollapseTimeout: Duration = .seconds(3)
}
```

### 5.4 ToolbarViewModel Integration
Properties in `ToolbarViewModel.swift`:
```swift
var selectedMode: ReadingMode = .standard
var selectedEnvironment: EnvironmentMode = .passthrough
var selectedLayout: LayoutMode = .landscape
```

### 5.5 ReadingView Integration
Attach via native ornament modifier in `ReadingView.swift`:

```swift
.ornament(attachmentAnchor: .scene(.bottom)) {
    ReadingToolbar()
}
```

---

## 6. Revision History & Post-Implementation Updates

### 6.1 Refactored Positioning to Native Ornaments
Removed manual 24pt vertical offsets. The toolbar positioning has been migrated to the native visionOS `.ornament` API to ensure flawless system-level centering and depth layering.

### 6.2 Material Changes  
Migrated from `Material.bar` to `.glassBackgroundEffect()` to establish visual consistency with the system's 3D glass rendering pipeline.

### 6.3 Fixed Hover Target Masking
Added `.contentShape(.hoverEffect, Capsule())` prior to applying `.hoverEffect(.highlight)`. This ensures that the hover state geometry dynamically adapts rendering as a perfect circle when collapsed and expanding smoothly into a capsule layout.

### 6.4 Fixed Collapsed Tap Target Competing with Window
**Problem:** On real Vision Pro hardware, tapping the upper half of the collapsed toolbar frequently failed to trigger expand. The lower half worked reliably. Root cause: the ornament's upper half overlaps with the window content area, where `ReadingTextView`'s `.scrollInputBehavior(.enabled, for: .look)` competes for gaze-based input resolution.

**Fix:** Moved tap handling from `collapsedContent` overlay to the shell level:
1. Added `.contentShape(Capsule())` on the shell for interaction hit-testing gives the entire glass surface tap priority over the window behind it
2. Added `.onTapGesture` at shell level (guarded by `!isExpanded`) the glass surface itself is the tap target
3. Simplified `collapsedContent` to icon-only (no frame, contentShape, or tap gesture needed)

This ensures the full 68pt circle is a single, shell-level tap target that the system resolves with higher priority than the overlapping window content.

### 6.5 ToolbarButton Selected State Color
**Change:** Selected button background color migrated from `DesignSystem.Color.tintAccent.opacity(0.15)` to `Color.white.opacity(0.15)` the native visionOS glass UI selected state background color. Icon and label foreground color for the selected state remains `DesignSystem.Color.tintAccent`

  1. Keeping a growing codebase under control

As the project grew, the hardest part was making sure new work didn't break what already worked. So I built a governance system into the project: components that are finished get "frozen," with explicit rules about what can and can't change, down to the single modifier that's allowed to stay flexible. It's how I keep a fast, AI-assisted workflow from quietly eroding the parts that are already solid.

Roadmap

Roadmap

Readspace is an ongoing project. A few directions I'm building toward:

AI-powered translationRead in one language with an instant translation in another. For anyone reading outside their native language.

AI reading extensions Reading often sparks curiosity that a book alone can't answer. This feature surfaces related context and knowledge based on what you're reading, and turning reading from passive intake into active exploration.

In-app environments Bringing environment switching directly into the app, making the immersive space fully part of Readspace itself.