Skip to main content

Co-Reading Experience

Table of Contents


Overview

Co-Reading transforms the solo story reading experience into a synchronized, collaborative activity during video calls. Multiple participants can select and read stories together, with one "owner" controlling navigation while others follow along in real-time.

Key Concepts

  • Owner/Follower Model: One participant controls navigation, others follow
  • Synchronized State: All participants see the same story location
  • Story Library: Shared collection of stories available to all participants
  • Real-time Sync: Navigation events broadcast via Experience framework
  • Resumable Sessions: Stories can be resumed where left off

Relationship to Other Components


Architecture Overview

Co-Reading implements the Experience framework pattern with these specific components:

┌─────────────────────────────────────────────────────────────────┐
│ Experience Framework │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ExperienceManager│ │ExperienceEventHub│ │ ExperienceSnapshot│ │
│ │ │ │ │ │ Manager │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ Co-Reading Implementation │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │CoReadingExperienceDefinition│ │ CoReadingSessionActor │ │
│ │ │ │ │ │
│ │ • Factory for components│────────▶│ • State: CoReadingState │ │
│ │ • Snapshot decoding │ │ • Actions: CoReadingAction │
│ │ • Service dependencies │ │ • Reducer: Redux pattern│ │
│ └─────────────────────────┘ │ • Effects: Side effects │ │
│ └─────────────┬───────────┘ │
│ │ │
│ ┌─────────────────────────┐ ┌─────────────▼───────────┐ │
│ │ CoReadingViewModel │◄────────┤ CoReadingState │ │
│ │ │ │ │ │
│ │ • UI coordination │ │ • Selected story │ │
│ │ • StoryReader binding │ │ • Current location │ │
│ │ • RTC integration │ │ • Participants │ │
│ │ • Owner/follower logic │ │ • Loading states │ │
│ └─────────────┬───────────┘ └─────────────────────────┘ │
│ │ │
└────────────────┼─────────────────────────────────────────────────┘

┌────────────────▼─────────────────────────────────────────────────┐
│ UI Layer │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
│ │CoReadingContainerView│ │ CoReadingLibraryView │ │
│ │ │ │ │ │
│ │ • State routing │ │ • Story carousel │ │
│ │ • Error handling │ │ • Participant names │ │
│ │ • Mode creation │ │ • Loading states │ │
│ └─────────────────────┘ └─────────────────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ ┌─────────────────────────────────┐ │
│ │ CoReadingMode │ │ UnifiedStoryReaderView │ │
│ │ │ │ │ │
│ │ • Video layout │ │ • Story presentation │ │
│ │ • Call controls │ │ • Navigation handling │ │
│ │ • Reader integration│ │ • Owner/follower UI differences │ │
│ └─────────────────────┘ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Component Breakdown

1. CoReadingState

Immutable state structure representing current co-reading session

CoReadingState (see State/CoReadingState.swift) tracks stories, selection, navigation location, participants, and loading states. The error field is excluded from serialization via custom Codable implementation.

Key Properties:

  • selectedStory: Computed property returning story metadata
  • hasLoadedPrerequisiteData: Whether stories are loaded
  • Custom Codable implementation excludes error from serialization

2. CoReadingAction

Actions that modify state through the reducer

CoReadingAction (see State/CoReadingAction.swift) defines user actions (synced) and system events (local only). User actions include story selection and navigation, while system events handle loading states and participant updates.

Synchronization:

  • shouldSync: Determines if action broadcasts to other participants
  • toSyncEvent(): Converts action to network event
  • from(syncEvent:): Creates action from received network event

3. CoReadingSessionActor

Redux-style state manager with async effects

Responsibilities:

  • Manages CoReadingState through coReadingReducer
  • Processes ExperienceEvents from event hub
  • Executes side effects (loading stories, network requests)
  • Provides state streams to view model
  • Handles participant changes from system context

Key Methods:

  • Public API: selectStory(), returnToLibrary(), loadStories(), processUserAction()
  • State observation: coReadingStateStream() and errorStream() for reactive updates

4. CoReadingViewModel

UI coordination layer bridging actor and SwiftUI

Responsibilities:

  • Subscribes to actor state streams
  • Provides actions for UI events
  • Manages StoryReaderViewModel binding for navigation sync
  • Handles participant name resolution
  • Coordinates owner/follower behavior differences

Key Features:

  • @Published var state: CoReadingState for SwiftUI binding
  • Navigation sync: Owner publishes, followers receive
  • RTC integration for participant names and video controls

5. CoReadingMode

ReaderModeDelegate implementation for story reader integration

Provides:

  • Video layout configuration
  • Call control buttons
  • Page view decorations (participant names, custom layout)
  • Owner/follower navigation permissions
  • Exit handling specific to co-reading

State Management

Redux Pattern Implementation

Co-Reading uses a pure Redux pattern with immutable state:

coReadingReducer (see State/CoReadingReducer.swift) takes current state and action, returns new state and optional side effect. Pure function with no side effects in the reducer itself.

Side Effects

Effects handle async operations outside the pure reducer:

CoReadingEffect (see State/CoReadingEffect.swift) defines side effects like loading library data and story metadata selection.

State Synchronization

  1. Owner Actions: Automatically broadcast via shouldSync flag
  2. Event Conversion: Actions convert to ExperienceEvent with JSON serialization
  3. Event Reception: Remote events convert back to actions
  4. State Update: Reducer applies action and publishes new state

Synchronization Flow

Owner Navigation Flow

1. Owner navigates in StoryReaderViewModel

2. CoReadingViewModel.navigationPublisher observes change

3. sessionActor.processUserAction(.updateLocation(location))

4. Reducer updates state.currentLocation

5. Action.toSyncEvent() creates ExperienceEvent

6. EventHub broadcasts via Firestore + PubSub

7. State published to UI (owner sees immediate update)

Follower Sync Flow

1. EventHub receives "location_update" event

2. CoReadingAction.from(syncEvent:) creates .updateLocation action

3. Reducer updates state.currentLocation

4. State stream notifies CoReadingViewModel

5. handleStateChange() detects location change

6. storyReaderViewModel.navigateToLocation() called

7. StoryReader updates UI (follower sees synced location)

Story Selection Flow

1. User taps story in library

2. viewModel.selectStory(story)

3. sessionActor processes .storySelectionStarted

4. Effect: .loadAndSelectStoryMetadata triggered

5. Load initial beat from repository

6. Determine starting StoryLocation

7. Process .selectStory and .updateLocation actions

8. Broadcast to other participants

9. UI transitions to UnifiedStoryReaderView

UI Components

CoReadingContainerView

Top-level routing component

Routes between three main states:

  • Error State: Shows error with retry button
  • Story Selected: Shows UnifiedStoryReaderView with CoReadingMode
  • Library State: Shows CoReadingLibraryView for story selection

Key Responsibilities:

  • Creates CoReadingMode delegate with RTC participants
  • Manages view hierarchy based on state
  • Handles error states and recovery

CoReadingLibraryView

Story selection carousel interface

Features:

  • Horizontal scrolling story grid
  • Async image loading with Firebase Storage
  • Participant-aware story filtering
  • Loading states (spinner, error, empty)
  • Story thumbnails with author names

Layout:

  • Bottom-anchored overlay design
  • Preserves video call UI underneath
  • Responsive to different screen sizes

Video Integration

CoReadingVideoFeedsView:

  • Displays local and remote video feeds
  • Configurable height ratio (25% by default)
  • Fallback to audio-only indicators
  • Integrates with StreamVideo SDK

Call Controls:

  • Mute/unmute audio and video
  • End call functionality
  • Visual state indicators
  • No launch experience button (already in experience)

Event Flow

Complete User Action Flow

User Action (tap story) 

CoReadingLibraryView.onSelectStory(story)

CoReadingViewModel.selectStory(story)

Task { await sessionActor.selectStory(story) }

sessionActor.handleCoReadingAction(.storySelectionStarted(storyId))

coReadingReducer(.storySelectionStarted) → (.loadAndSelectStoryMetadata effect)

sessionActor.loadAndSelectStorySideEffect()

storyService.fetchStory(id) & beatRepository.fetchBeat()

sessionActor.handleCoReadingAction(.selectStory(story))

coReadingReducer(.selectStory) → (update state.selectedStoryId)

sessionActor.handleCoReadingAction(.updateLocation(initialLocation))

coReadingReducer(.updateLocation) → (update state.currentLocation)

action.toSyncEvent() → ExperienceEventHub.send()

EventHub broadcasts to Firestore + PubSub

Other participants receive event → convert to action → update state

stateSubject.send(newState) → CoReadingViewModel state update

@Published var state triggers SwiftUI update

CoReadingContainerView shows UnifiedStoryReaderView
Owner: StoryReaderViewModel.userTappedToAdvance()

StoryNavigator.getPlan() → StoryPageController.executeNavigation()

UIPageViewController animates to new page

StoryReaderViewModel.navigationPublisher emits new location

CoReadingViewModel.navigationSubscription receives location

sessionActor.processUserAction(.updateLocation(location))

[Standard action processing: reducer → sync → broadcast]

Followers: receive event → update state → navigateToLocation()

StoryReaderViewModel.navigateToLocation() (bypasses owner checks)

StoryPageController.executeNavigation() on follower's UI

All participants now at same location

Integration with Story Reader

Binding Mechanism

CoReadingViewModel ↔ StoryReaderViewModel:

CoReadingViewModel.bindToStoryReader() establishes the connection. For owners, it subscribes to StoryReaderViewModel.navigationPublisher to capture navigation events for synchronization.

State Change Handling:

CoReadingViewModel.handleStateChange() responds to actor state changes. Owners sync to actor-determined locations on story selection completion, while followers navigate to owner's location changes.

ReaderModeDelegate Implementation

CoReadingMode provides:

  • isOwner: Controls navigation permissions in StoryReader
  • videoAreaView(): Custom video layout above story content
  • actionButtonsView(): Call controls specific to co-reading
  • pageView(): Story pages with participant names and custom sizing
  • getUserDisplayName(): Maps UserID to display names via RTC contacts

Owner:

  • Can tap, swipe, make choices in story
  • Navigation events automatically sync to followers
  • StoryReaderViewModel operates normally

Follower:

  • Navigation blocked at UI level (modeDelegate.isOwner checks)
  • Receives programmatic navigation from sync events
  • StoryReaderViewModel.navigateToLocation() bypasses owner checks
  • SwipeGestureRecognizer disabled (UIPageViewController dataSource = nil)

Development Guide

Adding New Co-Reading Features

  1. State Changes: Add new properties to CoReadingState
  2. User Actions: Add new cases to CoReadingAction with shouldSync flag
  3. Reducer Logic: Handle new actions in coReadingReducer
  4. Side Effects: Add effects to CoReadingEffect if async work needed
  5. UI Updates: Subscribe to state changes in CoReadingViewModel

Testing Strategies

State Management: Test reducer functions directly by calling coReadingReducer() with actions and verifying state changes and effects.

Actor Integration: Test actor behavior by calling processUserAction() and verifying state updates through actor.state.

Event Serialization: Test bidirectional conversion between CoReadingAction and ExperienceEvent using toSyncEvent() and from(syncEvent:) methods.

Performance Considerations

State Updates:

  • Redux pattern ensures immutable updates
  • removeDuplicates() prevents unnecessary UI refreshes
  • State comparison uses Equatable conformance

Network Optimization:

  • Only sync user actions (shouldSync flag)
  • JSON encoding/decoding for complex types
  • Firestore + PubSub dual-channel approach

Memory Management:

  • Actor pattern prevents retain cycles
  • Proper cancellation of async streams
  • Weak references in view model bindings

Error Handling Architecture

Co-Reading implements multi-layered error handling that preserves collaborative session integrity while providing clear user feedback and recovery options.

Error Types and Sources

1. Experience Framework Errors (ExperienceError)

  • Connection failures to Firestore/PubSub channels
  • Event serialization/deserialization failures
  • Participant management errors
  • Session snapshot corruption

2. Co-Reading Specific Errors (CoReadingError)

Defined in CoReadingSessionActor.swift - includes empty beats, missing stories, and beat loading failures.

3. Story Service Errors

  • Network failures when fetching story metadata
  • Permission/authorization issues
  • Story content unavailable or corrupted

4. StoryReader Navigation Errors

  • Beat loading failures during story reading
  • Invalid navigation requests
  • Content streaming interruptions

Error State Management

CoReadingState Error Tracking:

CoReadingState.error field stores current error state locally (excluded from participant synchronization via custom Codable implementation).

Error Propagation Through Redux:

Actions like storySelectionCompleted(error:) and loadingFinished(error:) carry error information. The reducer sets newState.error for UI display.

Error Stream Architecture:

CoReadingSessionActor.errorStream() converts all errors to ExperienceError for consistent handling. CoReadingViewModel subscribes and publishes via @Published var error.

Error Display Strategy

1. Library View Error Handling

CoReadingLibraryView Error States:

  • Loading Error: Network failures during story fetching
  • Empty Library: No stories available for participants
  • Selection Error: Failed to load selected story

See CoReadingContainerView.swift for error state routing and ErrorView implementation with retry actions.

ErrorView Components:

  • Warning icon and clear error description
  • "Try Again" button that calls loadStories()
  • Maintains collaborative session context
  • Doesn't exit co-reading experience

2. Story Reading Error Integration

Co-Reading delegates story reading errors to the underlying StoryReader system:

CoReadingMode.pageView() creates ErrorPageView for missing content, integrating with StoryReader's error handling architecture.

Story Reading Error Flow:

  • Beat loading failures → StoryReader LoadingStateManager
  • Navigation errors → StoryReader ErrorPageView
  • Network interruptions → Retry with timeout
  • Co-reading sync preserved during individual reading errors

Error Recovery Strategies

1. Automatic Recovery

Network Resilience: Multi-channel approach via ExperienceEventHub provides redundancy - PubSub for real-time sync, Firestore for eventual consistency, graceful degradation handling.

Session Preservation: Errors don't break co-reading sessions. Participant isolation ensures individual issues don't affect others. Owner/follower roles maintained during errors, sessions resumable across interruptions.

2. User-Initiated Recovery

Library Level Recovery:

  • "Try Again" - Reloads story library for all participants
  • Story Reselection - Choose different story if one fails
  • Session Restart - Owner can restart entire co-reading session

Story Level Recovery:

  • StoryReader "Return to Title" - Restart current story from beginning
  • Navigation Retry - Automatic retry of failed page navigation
  • Content Reloading - Background retry of failed content loading

Error Context and Participant Coordination

Owner Error Handling: Owner errors affect all participants. Error displayed to owner with recovery options, followers see loading state, owner recovery actions sync to all participants.

Follower Error Handling: Follower errors are isolated and don't affect owner. Error displayed locally, individual recovery possible, automatic sync restoration when resolved.

Error Isolation Boundaries:

  • Story Selection Errors: Affect all participants (collaborative action)
  • Navigation Errors: Owner errors propagate, follower errors isolated
  • Content Loading Errors: Individual participant responsibility
  • Network Errors: Graceful degradation with automatic reconnection

Error Prevention and Monitoring

Proactive Error Prevention:

  • Permission validation: Check isOwner before navigation actions
  • Prerequisites: Verify state.hasLoadedPrerequisiteData before story operations
  • Availability validation: Confirm story exists before selection attempts

Error Monitoring and Logging:

  • Comprehensive error logging in CoReadingSessionActor side effects
  • Error categorization for analytics (network vs content errors)
  • Context-aware logging with operation descriptions

Collaborative Error Scenarios

1. Owner Disconnects During Story

1. Follower detects owner departure via participant updates
2. Session transitions to degraded state (no new navigation)
3. Followers can continue reading at current location
4. UI indicates "waiting for host to return"
5. If owner returns, session resumes normally
6. If owner doesn't return, followers can exit to library

2. Story Content Unavailable

1. Owner selects story that fails to load
2. Error displayed to owner with "Try Again" option
3. Followers see loading state during owner's retry attempts
4. Owner can select different story to recover
5. New selection syncs to all participants

3. Network Partition During Reading

1. Real-time sync fails but session state preserved
2. Participants continue with last known state
3. Background reconnection attempts continue
4. When connection restored, states automatically reconcile
5. Conflicts resolved using event sequence numbers

Error State Debugging

Key State to Monitor:

  • Error propagation: state.error, experience manager errors, StoryReader loading errors
  • Error isolation: Verify owner vs follower error boundaries
  • Recovery state: storySelectionInProgressForStoryId, isLoadingLibrary flags

Error Recovery Testing:

  • Test error display by setting viewModel.state.error and verifying UI updates
  • Test error isolation by simulating follower errors and confirming owner unaffected
  • Verify session continuity during error conditions

This comprehensive error handling approach ensures that collaborative reading sessions remain robust and recoverable while providing clear feedback and actionable recovery options to users.


Common Issues & Debugging

Check:

  • Owner has isOwner = true in CoReadingMode
  • navigationSubscription is active in CoReadingViewModel
  • Network events being sent: check EventHub logs
  • Events being received: check actor's processReceivedEvent
  • StoryReaderViewModel binding established

Debug Commands:

  • Check ownership: coReadingMode.isOwner
  • Verify navigation subscription: viewModel.navigationSubscription != nil
  • Monitor state synchronization: state.currentLocation

"Story selection fails or hangs"

Check:

  • Story service returning valid stories
  • Beat repository can load initial beat
  • Network connectivity for story metadata
  • Error states being handled in UI

Debug Flow:

  1. Check state.storySelectionInProgressForStoryId during selection
  2. Monitor state.error for any failures
  3. Verify storySelectionCompleted action firing
  4. Check beat loading logs

"UI showing wrong state"

Common Causes:

  • State subscription not established
  • UI not observing @Published properties correctly
  • Race conditions in state updates

Fix:

  • Ensure state subscription via CoReadingViewModel.subscribeToActorState()
  • Verify @ObservedObject binding in SwiftUI views

"Participants not updating correctly"

Check:

  • ExperienceManager handling participant events
  • SystemContext updating participant list
  • Actor observing context changes

Debug:

  • Compare state.participants with systemContext.getParticipants()
  • Verify participant update events are being processed

"Video feeds not showing"

Check:

  • RTCViewModel providing valid CallParticipant objects
  • StreamVideo permissions granted
  • Video not muted or disabled
  • CoReadingMode configured with correct participants

Performance Issues

Symptoms:

  • UI lag during navigation
  • High memory usage
  • Slow story loading

Solutions:

  • Profile state update frequency
  • Check for memory leaks in actor subscriptions
  • Optimize image loading and caching
  • Monitor network request patterns

Debug Logging

Enable detailed logging:

  • Action processing: AppLogger.debug() in CoReadingSessionActor.handleCoReadingAction()
  • State transitions: Use action caseName property for readable logs
  • View model changes: Log state property changes in CoReadingViewModel.handleStateChange()

Key log patterns to watch:

  • Action processing in actor
  • State transitions in reducer
  • Event send/receive in hub
  • Navigation sync between view models
  • Error propagation through streams

This implementation demonstrates how the Experience framework enables complex collaborative features while maintaining clean separation of concerns and testable architecture patterns.