Article Recommendations

The ArticleRecommendationEngine analyzes your reading history to suggest unread articles you're likely to enjoy. It scores articles using two signals:

πŸ“Š Feed Preference

Tracks how often you read from each feed. Articles from feeds you engage with frequently get higher scores. If you've read 50 articles from "TechCrunch" but only 3 from "Cooking Weekly," tech articles rank higher.

πŸ”€ Keyword Affinity

Extracts keywords from titles of articles you've read. Common stop words are filtered out. If "machine learning" and "Swift" appear frequently in your history, articles with those terms get a boost.

How Scoring Works

Each unread article receives a composite score (0.0–1.0):

feedScore = readCount(feed) / maxReadCount        // 0.0 – 1.0
keywordScore = matchingKeywords / totalKeywords     // 0.0 – 1.0
finalScore = (0.6 Γ— feedScore) + (0.4 Γ— keywordScore)

Articles are returned sorted by score, with a recommendation reason explaining why each was suggested:

Usage

let engine = ArticleRecommendationEngine()
engine.readingHistory = ReadingHistoryManager.shared

// Get top 10 recommendations
let scored = engine.recommend(
    from: allUnreadStories,
    limit: 10,
    minScore: 0.1  // filter out low-confidence picks
)

for article in scored {
    print("\(article.story.title) β€” \(article.score)")
    print("  β†’ \(article.reason)")
}
Privacy: All recommendation data stays on-device. No reading history is sent to any server. The engine operates purely on local ReadingHistoryManager data persisted via NSSecureCoding.

Smart Feeds

SmartFeedManager lets users create saved keyword-based searches that automatically filter stories across all enabled feeds—like creating a custom "virtual feed" from keywords you care about.

Capabilities

FeatureDescription
Match Modes.any (OR) β€” match any keyword; .all (AND) β€” require all keywords
Search Scopes.titleOnly, .bodyOnly, .titleAndBody
Enable/DisableToggle feeds without deleting them
LimitsMax 10 smart feeds, up to 10 keywords each, max 100 chars per keyword
PersistenceNSSecureCoding to Documents directory

Creating a Smart Feed

let mgr = SmartFeedManager.shared

// Create a "Swift Development" smart feed
let feed = SmartFeed(
    name: "Swift Development",
    keywords: ["swift", "swiftui", "xcode", "ios"],
    matchMode: .any,
    searchScope: .titleAndBody
)

mgr.addSmartFeed(feed)  // persisted automatically

// Get matching stories
let matches = mgr.matchingStories(
    for: feed,
    from: allStories
)
print("Found \(matches.count) matching articles")

How Matching Works

Keyword matching is case-insensitive and uses localizedCaseInsensitiveContains for locale-aware comparison. With .any mode, a story matches if any keyword appears. With .all mode, every keyword must be present.

Digest Generator

The DigestGenerator creates formatted reading summaries—like a personal newsletter of what you've read. Supports multiple time windows and output formats.

Time Periods

Today Yesterday This Week Last Week This Month Last 7 Days Last 30 Days Custom Range

Output Formats

πŸ“ Plain Text

Simple text output with story titles, feed names, and timestamps. Great for copying to notes or sharing via text.

πŸ“‹ Markdown

Formatted with headers, lists, and links. Ready for pasting into Notion, Obsidian, or any Markdown editor.

🌐 HTML

Styled HTML digest with inline CSS. Share via email or view in a browser. Stories grouped by feed with clickable links.

Generating a Digest

let generator = DigestGenerator()
generator.readingHistory = ReadingHistoryManager.shared
generator.readingStats = ReadingStatsManager.shared

// Generate this week's digest as Markdown
let digest = generator.generate(
    period: .thisWeek,
    format: .markdown,
    includeStats: true  // append reading stats summary
)

print(digest.content)     // formatted string
print(digest.storyCount)  // number of articles included
print(digest.feedCount)   // number of feeds represented

Digest Structure

Each digest includes:

Keyword Alerts

KeywordAlertManager monitors incoming stories for specific terms and marks matching articles for attention. Unlike Smart Feeds (which are browsing filters), alerts are notifications—they surface articles you might miss.

Alert vs Smart Feed

FeatureKeyword AlertSmart Feed
PurposeNotify on new matchesBrowse filtered content
TriggerOn feed refreshOn-demand browsing
Match trackingCounts matches, last triggered dateNo tracking
Multi-keywordSingle keyword per alertUp to 10 keywords with AND/OR

Usage

let mgr = KeywordAlertManager.shared

// Create alert for "security vulnerability"
let alert = KeywordAlert(keyword: "security vulnerability")
mgr.addAlert(alert)

// Check new stories against all alerts
let triggered = mgr.checkStories(newStories)
for match in triggered {
    print("⚠️ Alert '\(match.alert.keyword)' triggered by: \(match.story.title)")
}

Reading Statistics

ReadingStatsManager tracks aggregate reading metrics over time. All data computed from ReadingHistoryManager entries.

Available Metrics

MetricDescription
totalArticlesReadLifetime article count
articlesReadTodayToday's count
averagePerDayDaily average over tracking period
topFeedsMost-read feeds ranked by article count
readingStreakConsecutive days with at least one article read
peakDayDay with most articles read
weekdayBreakdownArticles per day of week (Mon–Sun)

The ReadingStatsViewController displays these metrics with simple charts. Stats are updated each time ReadingHistoryManager records a read event.