One Evening, One RSS Reader: Magazine Redesign, Live Activities, and a Settings Panel That Actually Works

claude-codeai-developmentdevelopmentarchitectureios

NARSSR got a lot done tonight. Not “push one PR” a lot — more like “the app feels like a different app” a lot. Single session. Parallel agents. Ten modules touched. Here’s the recap.


What actually shipped

Quick inventory before the story:

  • Magazine-style home — hero image cards, favicon rail, sticky scroll header. The compact list still exists as a Settings toggle, but the default vibe is now Apple News / Feedbin weekend-digest, not 1998 Outlook Express.
  • 4-icon bottom tab bar — Home / Discover / Library / Profile, with scroll-hide. Liquid Glass everywhere.
  • Live Activities + Widget extension — refresh progress and reading sessions on the Dynamic Island + lock screen. Plus small/medium home-screen widgets showing unread counts and the latest hero article.
  • Five dead Settings toggles wired up — Storage Limit, Cache Images, WiFi Only, Refresh Interval, iCloud Sync. All five of those switches in the Settings panel were doing literally nothing before tonight. They look the same. They now do their jobs.
  • Font size + font family pipeline — Settings sliders now actually flow into ArticleStylesheet.swift and through to the WKWebView. Reading with JetBrains Mono at 20pt is a small thrill.
  • Read-dot bug fix — selecting an article wasn’t marking it read in the sidebar until refresh. Classic GRDB + SwiftUI observation gap. Fixed.
  • iCloud / CKSyncEngine schema audit — a proper forensic walk through CloudKitSync.swift, found 3 blockers, wrote the v2 proposal at docs/cloudkit-schema.md.
  • Deployed to a real iPhone via xcrun devicectl, not a simulator. The magazine home hits different on hardware.

The iCloud audit is the headline

I thought sync worked. It doesn’t. Or more precisely: it pulls, but it never pushes. The nextRecordZoneChangeBatch closure returns nil unconditionally. Every local write has been going into the void. Silently. For weeks.

Three blockers found:

  1. Record names use local GRDB rowids. Two devices will mint overlapping feed-123 identifiers and merge into each other’s data. Natural-key hashing required — feed-<sha256(url)>, etc.
  2. Foreign keys use local rowids too. Same problem, downstream. feedID: Int64 has no cross-device meaning.
  3. The push path is a no-op. The closure that maps pending record IDs to CKRecords returns nil unconditionally. Sync is pull-only today.

Plus 14 smaller findings — schema versioning, tombstones (deletions drop on the floor currently), clientModifiedAt for actual LWW conflict resolution, canonical wire schema across CloudKit / Nostr / SOLID so any backend can replay into any other. The doc is 300+ lines. I’m glad someone wrote it down before I tried to ship to TestFlight.

This is why you audit before you launch. A sync system that looks like it works is worse than one that obviously doesn’t.


Magazine home + Live Activities: shape matters

The old home was a List of ArticleRowView. It was fine. It looked like every other RSS reader from 2012.

The new home is hero cards — big lead image, title in the publisher’s voice, favicon + site name underneath, a favicon rail across the top letting you jump feeds without leaving the scroll. A sticky header that compacts on scroll. Compact mode still available via Settings → Appearance for people who want density over aesthetics.

Live Activities were the other fun one. A refresh-in-progress Activity with a progress bar on the Dynamic Island, plus a reading-session Activity that shows which article you’re in and how far. The widget target was already scaffolded — tonight we added two concrete widget configs (small = unread count, medium = latest hero article with image). WidgetKit + ActivityKit + NARSSRIntents all in the same target, which took about four build failures to get right.


Parallel agents as force multiplier

Real talk: I didn’t do this alone. I ran multiple Claude Code agents in parallel — one on the magazine redesign, one on Live Activities scaffolding, one on the CloudKit audit, one grinding through the Settings panel wiring. Each got a clean scope, their own working set of files, and a “ship it” deadline.

The magazine agent was working on ArticleListView.swift and ArticleRowView.swift. The Live Activities agent was inside a new Widget extension target entirely — different directory, different Info.plist, zero conflicts. The CloudKit agent was read-only and producing a markdown doc. The Settings agent was chasing five specific @AppStorage keys through to their consumers (CacheManager, SyncCoordinator, ArticleStylesheet).

The trick isn’t that they’re smart individually — they are, but so are a lot of tools. The trick is that four parallel streams of focused work compress time in a way sequential work doesn’t. Two hours of wall-clock looks like eight hours of output. On a complex app with ten modules, this is the only way I’d actually ship this much in an evening.

The coordination cost is real. I had to sequence the merges. The magazine agent touched ArticleStylesheet and so did the font-wiring agent. Resolving that was five minutes of me reading both diffs and picking the right one. Cheap compared to doing the work serially.


On-device AI: the tier decision

One design conversation tonight, not code: what’s the local LLM strategy? We landed on three tiers:

  • Tier 1: Foundation Models (@Generable ArticleAnalysis) for summary, topic, keyword extraction. Already wired via FoundationModelsService.swift. Apple ships the model. No download. Works on device.
  • Tier 2: NLTagger / NLEmbedding for NER, sentiment, topic similarity. Already shipped. These are fast, boring, reliable. Foundation Models fails over to these on devices where FM isn’t available or the prompt is too big.
  • Tier 3: Opt-in Gemma 3 download for users who want heavier-duty summarization or Q&A. This is a checkbox in Settings, a ~2GB download, MLX backend. Not default. Not shipped yet — just agreed on the shape.

The principle: no cloud API calls for classification, ever. Everything stays on the device. If you want better, you download more model. You don’t pay us a subscription for OpenAI credits.


What’s next

The iCloud audit doc has an 8-phase migration plan. That’s the next arc — get sync working for real, ship to TestFlight, get it on my iPad.

But tonight was a good night. The app went from “functionally complete RSS reader” to “RSS reader I actually want to use on my phone tomorrow morning.” That’s the magazine redesign doing work. That’s the Live Activity buzzing when my feeds refresh. That’s the read dot updating instantly when I tap.

Small stuff. All of it. Stacked up in one evening, it’s the difference between a side project and a product.

Onward.