Overview

FeedReader includes nine analytics modules that work together to give you a complete picture of your reading life. Every module stores data locally on-device with no cloud dependency.

ModulePurposeKey Metric
Speed TrackerMeasure and predict reading speedWords per minute (WPM)
Mood TrackerLog mood and correlate with feedsMood level (1–5)
Streak TrackerDaily reading consistencyConsecutive days reading
Insights GeneratorWeekly/monthly reading summariesReader personality archetype
Reading JournalAuto-generated daily reading diaryEntries with reflections
Time BudgetSet and track reading time limitsMinutes per day
Session TrackerIndividual reading session loggingDuration, articles, pauses
AchievementsGamified reading milestones40 badges, 5 tiers
Data ExportFull backup and restoreJSON archive with validation

📖 Speed Tracker

Tracks your words-per-minute (WPM) across articles, adjusted for article length, and provides personalized reading time estimates.

How It Works

When you finish an article, FeedReader records a speed sample with the article's word count, time spent, feed name, and category. Samples are filtered using IQR outlier detection (valid range: 50–1200 WPM) so unusually fast skims or long idle pauses don't skew your stats.

Length-Adjusted WPM

People read short and long articles at different speeds. The tracker maintains separate WPM buckets:

BucketWord CountMin Samples
Short< 500 words3
Medium500 – 1,500 words3
Long> 1,500 words3

estimatedReadingTime(wordCount:) picks the right bucket for the given article length, falling back to your global average if there aren't enough samples in that bucket yet.

API

let tracker = ReadingSpeedTracker.shared

// Record a reading sample
tracker.recordSample(
    articleTitle: "AI in 2026",
    feedName: "TechCrunch",
    wordCount: 1200,
    readingTimeSeconds: 300
)

// Get estimated reading time for a new article
let estimate = tracker.estimatedReadingTime(wordCount: 800)  // TimeInterval
let formatted = tracker.formattedEstimate(wordCount: 800)    // "3 min read"

// Trend analysis
let trend = tracker.speedTrend(days: 30)  // .improving, .declining, .stable

// Breakdowns
let byFeed = tracker.feedBreakdown()      // [SpeedBreakdown]
let byCat = tracker.categoryBreakdown()   // [SpeedBreakdown]

// Where you stand
let percentile = tracker.populationPercentile()  // 0-100
let label = tracker.speedLabel()                  // "Fast reader"

// Full profile
let profile = tracker.buildProfile()  // SpeedProfile

Speed Labels

WPM RangeLabel
< 150Careful reader
150 – 250Average reader
250 – 400Fast reader
> 400Speed reader

😊 Mood Tracker

Log how you feel while reading and discover which feeds boost or drain your mood.

Mood Levels

LevelValueEmoji
Very Low1😢
Low2😕
Neutral3😐
Good4🙂
Great5😄

Feed Impact Analysis

The tracker correlates mood changes with what you were reading. feedMoodImpact() returns a per-feed breakdown showing average mood delta — positive means the feed tends to improve your mood, negative means the opposite. moodBoosterFeeds(minSessions:) filters to feeds with consistently positive impact.

API

let mood = ReadingMoodTracker.shared

// Log a mood entry
mood.logMood(
    level: .good,
    note: "Interesting AI article",
    feedName: "Hacker News",
    articleLink: "https://..."
)

// Query
let today = mood.todayEntries()
let current = mood.currentMood()
let avg = mood.averageMood(from: weekAgo)  // Double?
let shifts = mood.moodShifts()             // [MoodShift]

// Discover mood-boosting feeds
let impacts = mood.feedMoodImpact()          // [FeedMoodImpact]
let boosters = mood.moodBoosterFeeds()       // positive-delta feeds only
let suggestions = mood.moodBasedSuggestions() // feed suggestions by mood

// Export
let report = mood.moodReport(from: monthAgo, to: Date())
let export = mood.exportEntries()  // JSON-compatible

🔥 Streak Tracker

Build a daily reading habit with streak tracking and weekly summaries.

How Streaks Work

A streak increments each consecutive day you read at least one article. Missing a day resets the streak counter to zero, but your best streak record is preserved. The optional goalTarget parameter lets you set a daily article count goal — the streak only counts days where you meet or exceed the target.

API

let streak = ReadingStreakTracker.shared

// Record reading activity (returns updated stats)
let stats = streak.recordArticleRead(goalTarget: 3)
// or record multiple at once:
let stats2 = streak.recordArticles(count: 5, goalTarget: 3)

// Current stats
let s = streak.getStats()
// s.currentStreak     — consecutive days reading
// s.bestStreak        — all-time longest streak
// s.totalArticlesRead — cumulative total
// s.todayCount        — articles read today
// s.goalMetToday      — whether today's goal is met

// Historical data
let record = streak.record(for: someDate)   // DailyReadingRecord?
let range = streak.records(from: start, to: end)

// Weekly summaries (articles per day, average, best day)
let weeks = streak.weeklySummaries(weeks: 4)

// Goal-based streak (days in a row meeting the goal)
let goalStreak = streak.goalStreakDays(last: 30)

💡 Insights Generator

Generates personalized weekly and monthly reading reports with reader personality profiling.

Report Contents

Reader Archetypes

ArchetypeProfile
📚 ScholarHigh volume, high diversity, consistent
🎯 SpecialistHigh volume, low diversity (deep in one area)
🦋 ExplorerModerate volume, high diversity
⚡ SpeedsterVery high WPM, skims widely
🧘 ContemplatorLow WPM, few articles, reads deeply
📰 News JunkieHigh volume, mostly news feeds
🌊 CasualLight reader, sporadic schedule
🏃 CommuterReads in concentrated bursts (peak hours)

API

let insights = ReadingInsightsGenerator.shared

// Generate a report
let report = insights.generateWeeklyInsight(
    from: readEvents,
    endDate: Date()
)

let monthly = insights.generateMonthlyInsight(
    from: readEvents,
    endDate: Date()
)

// Latest saved report
let last = insights.latestReport(for: .weekly)

// Export
let text = insights.exportAsText(report)
let json = insights.exportAsJSON(report)

📓 Reading Journal

An auto-generated daily diary of your reading activity — articles read, highlights saved, notes taken, and personal reflections.

Journal Entry Structure

Export Formats

Journal entries can be exported as Markdown (suitable for Obsidian, Notion, or GitHub) or JSON. Supports single-day, date-range, or full-history export.

API

let journal = ReadingJournalManager.shared

// Automatic recording (called by FeedReader internally)
journal.recordArticleRead(
    link: url, title: title,
    feedName: feed, timeSpent: seconds
)
journal.recordHighlight(text: excerpt, articleTitle: title)
journal.recordNote(text: note, articleTitle: title)

// Manual entries
journal.addReflection("Great deep-dive day")
journal.addMoodTag(.positive)

// Query
let today = journal.todayEntry()
let entry = journal.entry(for: someDate)
let range = journal.entries(from: start, to: end)

// Export
let md = journal.exportMarkdown(for: someDate)
let mdRange = journal.exportMarkdownRange(from: start, to: end)
let json = journal.exportJSON()

// Digests
let weekly = journal.weeklyDigest()
let monthly = journal.monthlyDigest()

// Streaks
let streak = journal.currentStreak  // consecutive days with entries

⏱️ Time Budget

Set daily reading time goals and track how your time is distributed across feeds and categories.

Configuration

let budget = ReadingTimeBudget.shared

// Set a daily target (in minutes)
var config = budget.getConfig()
config.dailyTargetMinutes = 30
config.warningThresholdPercent = 80  // warn at 80% of budget
budget.updateConfig(config)

Session Tracking

Start a session when the user begins reading and stop it when they leave. Sessions automatically record duration, article context, and word count.

// Start/stop reading
budget.startSession(
    articleTitle: "AI Safety in Practice",
    feedName: "Alignment Forum",
    wordCount: 2400
)
let entry = budget.stopSession()

// Query entries
let todayEntries = budget.entries(on: Date())
let weekEntries = budget.entries(from: weekAgo, to: Date())

// Time analysis
let todayTotal = budget.todayTotal()       // TimeInterval
let remaining = budget.remainingToday()     // TimeInterval
let progress = budget.dailyProgress()       // 0.0 – 1.0+
let exceeded = budget.isOverBudget()        // Bool

// Breakdowns
let byFeed = budget.feedBreakdown(from: weekAgo, to: Date())
let byCat = budget.categoryBreakdown(from: weekAgo, to: Date())
let byHour = budget.hourlyDistribution(days: 7)
let trend = budget.dailyTrend(days: 14)

// Suggestions
let suggestions = budget.budgetSuggestions()  // personalized tips

🎧 Session Tracker

Track individual reading sessions with pause/resume support and per-article timing.

Session Lifecycle

  1. startSession() — begins a new session (optional tag for context)
  2. startArticle(link:title:feedName:) — mark when an article is opened
  3. finishArticle() — mark when the article is closed or scrolled past
  4. pauseSession() / resumeSession() — handle app backgrounding
  5. endSession() — finalize and persist the session

Sessions record total duration, active reading time (excluding pauses), articles read with per-article time, and optional tags for categorization.

API

let sessions = ReadingSessionTracker.shared

// Basic flow
let session = sessions.startSession(tag: "morning-reading")
sessions.startArticle(link: url, title: title, feedName: feed)
sessions.finishArticle()
let completed = sessions.endSession()

// Query
let recent = sessions.sessions(lastDays: 7)
let tagged = sessions.sessions(withTag: "morning-reading")
let byId = sessions.session(byId: sessionId)

// Summaries
let summary = sessions.summary(for: recent)
// summary.totalDuration, summary.activeTime, summary.totalArticles,
// summary.avgArticlesPerSession, summary.avgSessionDuration

🏆 Achievements

Gamified reading milestones with 40 badges across 12 categories and 5 rarity tiers.

Rarity Tiers

TierPointsDifficulty
🟢 Common10Easy milestones (read 1 article)
🔵 Uncommon25Regular effort (7-day streak)
🟣 Rare50Dedicated readers (100 articles)
🟠 Epic100Exceptional (30-day streak)
🔴 Legendary250Elite (1000+ articles, 100-day streak)

Categories

Achievements span reading volume, consistency (streaks), speed, diversity, bookmarking, sharing, time-of-day patterns, and more. Secret achievements are hidden until unlocked.

API

let achievements = ReadingAchievementsManager.shared

// Check progress
let all = achievements.allProgress()
let unlocked = achievements.unlockedAchievements()
let locked = achievements.lockedAchievements(includeSecrets: true)

// Filter
let streakBadges = achievements.achievements(in: .consistency)
let epics = achievements.achievements(ofRarity: .epic)

// Leveling
let level = achievements.currentLevel()
let points = achievements.totalPoints()
let toNext = achievements.pointsToNextLevel()

// Text summary
let summary = achievements.textSummary()

💾 Data Export & Import

Full backup and restore for all reading data — speed samples, mood entries, streaks, journal, time budget, sessions, achievements, bookmarks, highlights, notes, tags, and more.

Export Options

You can export everything (.all) or a specific subset:

OptionData Included
.allEvery data category below
.readingHistoryArticles read, timestamps, durations
.bookmarksSaved articles
.highlightsArticle highlights with colors
.notesPersonal annotations
.tagsArticle tags
.goalsReading goals and progress
.analyticsSpeed, mood, streaks, sessions
.journalJournal entries with reflections
.achievementsBadge progress and unlock dates

Import Strategies

StrategyBehavior
.skipKeep existing data, skip conflicts
.overwriteReplace existing with imported data
.mergeCombine both, preferring newer timestamps

API

let exporter = ReadingDataExporter.shared

// Export
let (data, error) = exporter.exportData(options: .all)
let (fileSize, err) = exporter.exportToFile(url: backupUrl)

// Validate before importing
let warnings = exporter.validateArchive(data)
let preview = exporter.previewArchive(data)
// preview.version, preview.exportDate, preview.itemCounts

// Import
let result = exporter.importData(data, strategy: .merge)
// result.imported, result.skipped, result.errors