Co-Reading Experience
Table of Contents
- Overview
- Architecture Overview
- Component Breakdown
- State Management
- Synchronization Flow
- UI Components
- Event Flow
- Integration with Story Reader
- Development Guide
- Common Issues & Debugging
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
- Builds on: SharedExperiences Framework for multi-channel synchronization
- Integrates with: StoryReader for actual story presentation
- Extends: Solo reading with collaborative features
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 metadatahasLoadedPrerequisiteData: Whether stories are loaded- Custom
Codableimplementation excludeserrorfrom 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 participantstoSyncEvent(): Converts action to network eventfrom(syncEvent:): Creates action from received network event
3. CoReadingSessionActor
Redux-style state manager with async effects
Responsibilities:
- Manages
CoReadingStatethroughcoReadingReducer - 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()anderrorStream()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: CoReadingStatefor 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
- Owner Actions: Automatically broadcast via
shouldSyncflag - Event Conversion: Actions convert to
ExperienceEventwith JSON serialization - Event Reception: Remote events convert back to actions
- 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
UnifiedStoryReaderViewwithCoReadingMode - Library State: Shows
CoReadingLibraryViewfor story selection
Key Responsibilities:
- Creates
CoReadingModedelegate 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
Navigation Event Flow
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 StoryReadervideoAreaView(): Custom video layout above story contentactionButtonsView(): Call controls specific to co-readingpageView(): Story pages with participant names and custom sizinggetUserDisplayName(): Maps UserID to display names via RTC contacts
Navigation Permission Model
Owner:
- Can tap, swipe, make choices in story
- Navigation events automatically sync to followers
- StoryReaderViewModel operates normally
Follower:
- Navigation blocked at UI level (
modeDelegate.isOwnerchecks) - Receives programmatic navigation from sync events
- StoryReaderViewModel.navigateToLocation() bypasses owner checks
- SwipeGestureRecognizer disabled (UIPageViewController dataSource = nil)
Development Guide
Adding New Co-Reading Features
- State Changes: Add new properties to
CoReadingState - User Actions: Add new cases to
CoReadingActionwithshouldSyncflag - Reducer Logic: Handle new actions in
coReadingReducer - Side Effects: Add effects to
CoReadingEffectif async work needed - 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
Equatableconformance
Network Optimization:
- Only sync user actions (
shouldSyncflag) - 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
isOwnerbefore navigation actions - Prerequisites: Verify
state.hasLoadedPrerequisiteDatabefore story operations - Availability validation: Confirm story exists before selection attempts
Error Monitoring and Logging:
- Comprehensive error logging in
CoReadingSessionActorside 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,isLoadingLibraryflags
Error Recovery Testing:
- Test error display by setting
viewModel.state.errorand 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
"Navigation not syncing between participants"
Check:
- Owner has
isOwner = truein CoReadingMode -
navigationSubscriptionis 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:
- Check
state.storySelectionInProgressForStoryIdduring selection - Monitor
state.errorfor any failures - Verify
storySelectionCompletedaction firing - Check beat loading logs
"UI showing wrong state"
Common Causes:
- State subscription not established
- UI not observing
@Publishedproperties correctly - Race conditions in state updates
Fix:
- Ensure state subscription via
CoReadingViewModel.subscribeToActorState() - Verify
@ObservedObjectbinding in SwiftUI views
"Participants not updating correctly"
Check:
- ExperienceManager handling participant events
- SystemContext updating participant list
- Actor observing context changes
Debug:
- Compare
state.participantswithsystemContext.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()inCoReadingSessionActor.handleCoReadingAction() - State transitions: Use action
caseNameproperty 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.