class

RSSParser

Parses RSS XML feeds into RSSStory objects. Supports loading multiple feeds concurrently with O(1) deduplication by link URL. Thread-safe โ€” all shared state mutations are serialized on an internal serial queue.

Properties

PropertyTypeDescription
delegateRSSParserDelegate?Delegate notified when feed loading completes or fails
stories[RSSStory]Read-only. Accumulated stories from all parsed feeds

Methods

MethodDescription
loadFeeds(_ urls: [String]) Parse stories from multiple feed URLs concurrently. Calls delegate on the main thread when all feeds complete. Cancels any in-flight load from a previous call. Stories are deduplicated by link URL.
parseData(_ data: Data) โ†’ [RSSStory] Parse stories from in-memory XML data synchronously. Useful for testing and offline scenarios. Returns an array of parsed stories directly.

Usage

import FeedReaderCore class MyFeedController: RSSParserDelegate { let parser = RSSParser() func fetchFeeds() { parser.delegate = self parser.loadFeeds([ "https://feeds.bbci.co.uk/news/world/rss.xml", "https://techcrunch.com/feed/" ]) } func parserDidFinishLoading(stories: [RSSStory]) { // Stories from all feeds, deduplicated print("Loaded \(stories.count) stories") } func parserDidFailWithError(_ error: Error?) { print("Feed load failed: \(error?.localizedDescription ?? "unknown")") } }
protocol

RSSParserDelegate

Delegate protocol for receiving RSS parsing results. Callbacks are always delivered on the main thread.

Required Methods

MethodDescription
parserDidFinishLoading(stories: [RSSStory]) Called when all requested feeds have finished loading. The stories array contains deduplicated results from all feeds.
parserDidFailWithError(_ error: Error?) Called when a feed fails to load. The error may be nil for non-HTTP errors.
class

RSSStory

Represents a single parsed RSS story with title, body, link, and optional image URL. Provides URL validation and HTML sanitization. Conforms to NSObject and Sendable.

Properties

PropertyTypeDescription
titleStringThe story headline
bodyStringThe story description with HTML tags stripped and entities decoded
linkStringThe story's unique URL (used for equality and deduplication)
imagePathString?Optional thumbnail image URL (only set if URL passes safety validation)

Initializer

public init?( title: String, body: String, link: String, imagePath: String? = nil )

Returns nil if:

  • title is empty
  • body is empty after HTML stripping
  • link is not a valid HTTP/HTTPS URL

Static Methods

MethodDescription
isSafeURL(_ urlString: String?) โ†’ Bool Validates that a URL uses only allowed schemes (http, https). Rejects javascript:, file:, data:, and other unsafe schemes.
stripHTML(_ html: String) โ†’ String Strips HTML tags via regex and decodes common HTML entities (&, <, >, ", ',  ).

Equality

Two RSSStory instances are equal if their link properties match. This is how deduplication works across multiple feeds.

class

FeedItem

Represents an RSS feed source with a name, URL, and enabled state. Conforms to NSSecureCoding for persistent storage and Sendable for thread safety.

Properties

PropertyTypeDescription
nameStringDisplay name for the feed
urlStringRSS feed URL string
isEnabledBoolWhether the feed is currently enabled for fetching
identifierStringComputed. Lowercased URL used for deduplication

Initializer

public init( name: String, url: String, isEnabled: Bool = false )

Static Properties

PropertyDescription
presets: [FeedItem] 10 built-in feed sources: BBC World News, BBC Technology, BBC Science, BBC Business, NPR News, Reuters World, TechCrunch, Ars Technica, Hacker News, The Verge

Usage

// Use built-in presets let feeds = FeedItem.presets let enabledUrls = feeds .filter { $0.isEnabled } .map { $0.url } // Create custom feed let custom = FeedItem( name: "My Blog", url: "https://myblog.com/rss.xml", isEnabled: true )
enum

NetworkReachability

Provides a simple check for network connectivity using SystemConfiguration. Uses SCNetworkReachability to check for a default network route.

Static Methods

MethodDescription
isConnected() โ†’ Bool Returns true if the device currently has a network route available. Does not guarantee that a specific host is reachable โ€” only that the system believes a route exists.

Usage

import FeedReaderCore if NetworkReachability.isConnected() { parser.loadFeeds(feedUrls) } else { // Load from cache or show offline UI showCachedStories() }

ArticleArchiveExporter

Exports RSSStory objects as self-contained HTML archive files for offline reading, sharing, or long-term preservation. Supports single and batch export with 4 visual themes (light, dark, sepia, newspaper).

Initialization

let exporter = ArticleArchiveExporter(options: .default) // Or with custom options: var opts = ArticleArchiveExporter.ExportOptions( theme: .sepia, includeMetadata: true, includeTableOfContents: true, includeWordCount: true, includeEstimatedReadTime: true ) let exporter = ArticleArchiveExporter(options: opts)

Types

TypeDescription
ThemeVisual theme enum: .light, .dark, .sepia, .newspaper. Each provides backgroundColor, textColor, accentColor, and fontFamily.
ExportOptionsConfigures theme, metadata inclusion, table of contents, word count, read time, and optional custom CSS.
ExportResultContains the exported filename, htmlContent, articleCount, totalWordCount, and exportDate.

Methods

MethodDescription
exportArticle(_ story: RSSStory) โ†’ ExportResultExport a single article as a standalone HTML file.
exportArticles(_ stories: [RSSStory]) โ†’ ExportResult?Batch export multiple articles into a single HTML file with table of contents. Returns nil if the array is empty.
save(_ result: ExportResult, to directory: URL) โ†’ URL?Write the export result to disk. Returns the file URL on success.
listArchives(in directory: URL) โ†’ [(filename, date, size)]List all .html archive files in a directory.
deleteArchive(named:in:) โ†’ BoolDelete an archive file by name.
countWords(_ text: String) โ†’ IntCount words in a text string.
estimateReadTime(wordCount: Int) โ†’ IntEstimate reading time in minutes (assumes 200 wpm).

Usage

let exporter = ArticleArchiveExporter(options: .init(theme: .dark)) let result = exporter.exportArticle(story) if let url = exporter.save(result, to: archiveDir) { print("Saved to \(url)") }

FeedHealthMonitor

Monitors feed health by analyzing article publication dates. Classifies feeds as healthy, warning, stale, or dead and generates actionable reports with scores and recommendations.

Types

TypeDescription
FeedHealthStatusEnum: .healthy, .warning, .stale, .dead. Comparable by severity.
FeedHealthResultPer-feed result with status, score (0โ€“100), daysSinceLastArticle, averageUpdateInterval, issues, and recommendations.
FeedHealthReportAggregate report with results, overallScore, overallStatus, statusCounts, and JSON export via jsonDict.
FeedHealthConfigConfigures thresholds: warningDays, staleDays, deadDays, minimumArticleCount, maxUpdateIntervalDays.

Methods

MethodDescription
checkFeed(feedName:feedURL:articleDates:) โ†’ FeedHealthResultAnalyze a single feed's health based on its article publication dates.
generateReport(feeds:) โ†’ FeedHealthReportGenerate a comprehensive health report for multiple feeds at once.
detectTrend(articleDates:) โ†’ Double?Detect publishing frequency trend. Returns a multiplier (>1 = accelerating, <1 = decelerating).

Usage

let monitor = FeedHealthMonitor() let result = monitor.checkFeed( feedName: "Swift Blog", feedURL: "https://swift.org/blog/feed.xml", articleDates: dates ) print(result.summary) // "Swift Blog: healthy (92/100)"

KeywordExtractor

Extracts keywords and themes from article text using TF-based frequency analysis with stop-word filtering. Works on individual stories or across collections for theme detection.

Properties

PropertyDescription
minimumWordLength: IntMinimum character length for a word to be considered (default: 3).
defaultCount: IntDefault number of keywords to return (default: 5).

Methods

MethodDescription
extractKeywords(from text: String, count: Int?) โ†’ [String]Extract top keywords from raw text.
extractTags(from story: RSSStory, count: Int?) โ†’ [String]Extract keyword tags from a story's title and body.
extractThemes(from stories: [RSSStory], count: Int?) โ†’ [String]Extract common themes across a collection of stories.

Usage

let extractor = KeywordExtractor() let tags = extractor.extractTags(from: story, count: 8) // ["swift", "concurrency", "async", "await", ...] let themes = extractor.extractThemes(from: allStories) // ["programming", "apple", "ios", "development", "swift"]