π§ Smart Features
FeedReader goes beyond basic RSS reading with intelligent features that learn from your habits, surface relevant articles, and generate personalized digests—all running entirely on-device.
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:
.preferredFeedβ "You've read 42 articles from TechCrunch".keywordMatchβ "Matches your interests: Swift, iOS, Xcode".frequentlyRevisitedβ Similar to articles you've re-read.combinedβ Multiple signals contributed
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)")
}
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
| Feature | Description |
|---|---|
| Match Modes | .any (OR) β match any keyword; .all (AND) β require all keywords |
| Search Scopes | .titleOnly, .bodyOnly, .titleAndBody |
| Enable/Disable | Toggle feeds without deleting them |
| Limits | Max 10 smart feeds, up to 10 keywords each, max 100 chars per keyword |
| Persistence | NSSecureCoding 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
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:
- Header β period name and date range
- Stories by Feed β grouped by source feed, with titles, publication dates, and links
- Reading Stats (optional) β total articles read, average per day, top feeds, most active days
- Footer β generation timestamp
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
| Feature | Keyword Alert | Smart Feed |
|---|---|---|
| Purpose | Notify on new matches | Browse filtered content |
| Trigger | On feed refresh | On-demand browsing |
| Match tracking | Counts matches, last triggered date | No tracking |
| Multi-keyword | Single keyword per alert | Up 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
| Metric | Description |
|---|---|
totalArticlesRead | Lifetime article count |
articlesReadToday | Today's count |
averagePerDay | Daily average over tracking period |
topFeeds | Most-read feeds ranked by article count |
readingStreak | Consecutive days with at least one article read |
peakDay | Day with most articles read |
weekdayBreakdown | Articles per day of week (MonβSun) |
The ReadingStatsViewController displays these metrics with simple charts. Stats are updated each time ReadingHistoryManager records a read event.