Project Structure

FeedReader is split into two layers: a platform-independent Swift Package and an iOS app that uses it.

FeedReader/ β”‚ β”œβ”€β”€ Sources/FeedReaderCore/ ← Swift Package (no UIKit) β”‚ β”œβ”€β”€ RSSParser.swift RSS parsing with concurrent loading β”‚ β”œβ”€β”€ RSSStory.swift Story model with URL/HTML safety β”‚ β”œβ”€β”€ FeedItem.swift Feed source model + presets β”‚ └── NetworkReachability.swift SCNetworkReachability wrapper β”‚ β”œβ”€β”€ FeedReader/ ← iOS App (UIKit) β”‚ β”œβ”€β”€ AppDelegate.swift App lifecycle β”‚ β”œβ”€β”€ Story.swift NSCoding-based story model β”‚ β”œβ”€β”€ Feed.swift Feed model (app layer) β”‚ β”œβ”€β”€ FeedManager.swift Feed source CRUD singleton β”‚ β”œβ”€β”€ BookmarkManager.swift Bookmark persistence singleton β”‚ β”œβ”€β”€ RSSFeedParser.swift App-layer XML parser β”‚ β”œβ”€β”€ ImageCache.swift Async image loader + NSCache β”‚ β”œβ”€β”€ Reachability.swift Network detection β”‚ β”œβ”€β”€ StoryTableViewController Main feed list β”‚ β”œβ”€β”€ StoryViewController Story detail β”‚ β”œβ”€β”€ BookmarksViewController Saved stories β”‚ β”œβ”€β”€ FeedListViewController Feed manager UI β”‚ └── NoInternetFound... Offline fallback β”‚ β”œβ”€β”€ FeedReaderTests/ ← App tests β”œβ”€β”€ Tests/FeedReaderCoreTests/ ← Package tests └── Package.swift SPM manifest

Data Flow

Here's how data flows through the app from network to screen:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” HTTP GET β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ RSS Feed β”‚ ◄──────────────── β”‚ URLSession β”‚ β”‚ (XML) β”‚ ──────────────► β”‚ (async) β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ XML data β”‚ β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ XMLParser β”‚ β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ (per-feed β”‚ β”‚ collector) β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ [RSSStory] β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” deduplicate β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ RSSParser β”‚ ─────────────────►│ Main Thread β”‚ β”‚ (merge + β”‚ via serial β”‚ Delegate β”‚ β”‚ dedup) β”‚ queue β”‚ Callback β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ UITableViewβ”‚ β”‚ NSCoding β”‚ β”‚ ImageCache β”‚ β”‚ (display) β”‚ β”‚ (persist) β”‚ β”‚ (thumbnailsβ”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Threading Model

FeedReader's threading is designed for correctness without complexity:

ComponentThreadWhy
URLSession tasksBackground (system)Network I/O should never block the main thread
RSSParseCollectorURLSession callback threadEach feed parsed in isolation β€” no shared state
RSSParser mergeSerial DispatchQueueProtects shared stories and seenLinks from data races
Delegate callbacksMain threadUI updates must happen on main thread
ImageCache downloadsBackground (URLSession)Image fetching doesn't block scrolling
NSCoding persistenceMain threadSimple archiving β€” fast enough for typical story counts

Concurrency Safety

The key concurrency challenge is multi-feed parsing. When loadFeeds is called with multiple URLs:

  1. Each URL spawns its own URLSession data task
  2. Each task's completion handler creates an isolated RSSParseCollector β€” no shared parsing state
  3. Parsed stories are merged onto a serial DispatchQueue, which protects the shared stories array and seenLinks set
  4. When the last feed completes, the delegate is called on the main thread
  5. If a new loadFeeds call arrives while a previous one is in-flight, the previous session is cancelled via invalidateAndCancel()

Caching Strategy

Story Persistence (NSCoding)

Stories are persisted to disk using NSKeyedArchiver/NSKeyedUnarchiver. This provides:

Image Caching (NSCache)

The ImageCache class provides a two-tier caching strategy:

MVC Pattern

The iOS app follows Apple's recommended MVC architecture:

LayerComponentsResponsibility
Model Story, Feed Data representation, NSCoding conformance, equality
View Storyboards, StoryTableViewCell Visual layout, cell rendering, image display
Controller StoryTableViewController, StoryViewController, etc. User interaction, data binding, navigation, XML parsing orchestration

Singletons

Two singletons manage persistent state:

The Swift Package Split

The FeedReaderCore Swift Package extracts platform-independent functionality:

In Package (FeedReaderCore)In App (FeedReader)
RSSParser β€” concurrent multi-feed parsingStoryTableViewController β€” UI + XML orchestration
RSSStory β€” model with URL/HTML safetyStory β€” NSCoding model for persistence
FeedItem β€” feed source model + presetsFeed β€” app-layer feed model
NetworkReachability β€” SCNetworkReachabilityReachability β€” app-layer connectivity

The package has no UIKit dependency, making it suitable for any iOS, macOS, or server-side Swift project that needs RSS parsing.

Security Considerations

URL Validation

All URLs (story links and image paths) are validated by RSSStory.isSafeURL():

HTML Sanitization

RSS descriptions often contain HTML. RSSStory.stripHTML() removes all HTML tags via regex and decodes common entities, preventing XSS-style injection in the UI.

Network Security

All feed fetching uses URLSession with default security settings (ATS enforced on iOS 9+). HTTPS is preferred for all preset feeds.