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:
| Component | Thread | Why |
URLSession tasks | Background (system) | Network I/O should never block the main thread |
RSSParseCollector | URLSession callback thread | Each feed parsed in isolation β no shared state |
RSSParser merge | Serial DispatchQueue | Protects shared stories and seenLinks from data races |
| Delegate callbacks | Main thread | UI updates must happen on main thread |
ImageCache downloads | Background (URLSession) | Image fetching doesn't block scrolling |
NSCoding persistence | Main thread | Simple archiving β fast enough for typical story counts |
Concurrency Safety
The key concurrency challenge is multi-feed parsing. When loadFeeds is called with multiple URLs:
- Each URL spawns its own
URLSession data task
- Each task's completion handler creates an isolated
RSSParseCollector β no shared parsing state
- Parsed stories are merged onto a serial
DispatchQueue, which protects the shared stories array and seenLinks set
- When the last feed completes, the delegate is called on the main thread
- 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:
- Offline reading β Previously fetched stories are available without network
- Fast launch β Cached stories display immediately while fresh data loads
- Graceful degradation β If a feed fails to load, the user still sees cached content
Image Caching (NSCache)
The ImageCache class provides a two-tier caching strategy:
- In-memory (NSCache) β Images are cached in memory for instant access during scrolling. NSCache automatically evicts under memory pressure.
- Async loading β Images are fetched on background threads. The cell checks if the image is already cached before making a network request.
- Cell reuse safety β The cache key is checked against the current cell's story to prevent displaying stale images from reused cells.
MVC Pattern
The iOS app follows Apple's recommended MVC architecture:
| Layer | Components | Responsibility |
| 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:
FeedManager.shared β Manages the list of RSS feed sources. Handles adding, removing, toggling, and reordering feeds. Persists to UserDefaults via NSCoding.
BookmarkManager.shared β Manages bookmarked stories. Persists to the app's documents directory via NSCoding.
The Swift Package Split
The FeedReaderCore Swift Package extracts platform-independent functionality:
| In Package (FeedReaderCore) | In App (FeedReader) |
RSSParser β concurrent multi-feed parsing | StoryTableViewController β UI + XML orchestration |
RSSStory β model with URL/HTML safety | Story β NSCoding model for persistence |
FeedItem β feed source model + presets | Feed β app-layer feed model |
NetworkReachability β SCNetworkReachability | Reachability β 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():
- Only
http and https schemes are allowed
javascript:, file:, data:, and other schemes are rejected
- Stories with invalid link URLs are dropped entirely (
init? returns nil)
- Invalid image URLs are silently discarded (imagePath set to
nil)
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.