feat: Phase 1 auto-scroll engine for Reader feed
All checks were successful
Security Checks / dependency-audit (push) Successful in 23s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s

ENGINE (ScrollViewDriver.swift):
- UIViewRepresentable placed inside ScrollView (zero size)
- Finds parent UIScrollView via view hierarchy traversal
- CADisplayLink at 60fps drives contentOffset.y smoothly
- Speed: 1.0x = 60pt/sec, adjustable 0.25x–3.0x in 0.25 steps
- User touch detection: intercepts UIScrollViewDelegate
  scrollViewWillBeginDragging → stops auto-scroll immediately
- Stops at bottom (contentOffset >= maxOffset)
- Forwards all delegate methods to SwiftUI's original delegate

INTEGRATION (EntryListView):
- Accepts @Binding isAutoScrolling + scrollSpeed
- ScrollViewDriver placed as first child in ScrollView
- Auto-scroll stops on: user touch, navigation back (onAppear),
  filter change, sub-tab change, reaching bottom

CONTROLS (ReaderTabView — temporary, Phase 1):
- Play/Stop button in toolbar (play.fill / stop.fill)
- When playing: [-] speed [+] controls appear inline
- Speed shown as "1.00x" with monospacedDigit

MARK-AS-READ:
- Auto-scroll drives real UIScrollView contentOffset
- This moves LazyVStack rows, triggering their GeometryReader
  onChange callbacks — the existing mark-as-read system fires
  naturally with no special case or bypass needed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-04 06:51:08 -05:00
parent 6ed7f8a230
commit 85c3bb7a42
4 changed files with 226 additions and 1 deletions

View File

@@ -50,6 +50,7 @@
A10045 /* ArticleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10045 /* ArticleView.swift */; };
A10046 /* ArticleWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10046 /* ArticleWebView.swift */; };
A10047 /* FeedManagementSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10047 /* FeedManagementSheet.swift */; };
A10048 /* ScrollViewDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10048 /* ScrollViewDriver.swift */; };
F20549752F805F5800AE8DF5 /* ConfettiSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = F20549742F805F5800AE8DF5 /* ConfettiSwiftUI */; };
/* End PBXBuildFile section */
@@ -97,6 +98,7 @@
B10045 /* ArticleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleView.swift; sourceTree = "<group>"; };
B10046 /* ArticleWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleWebView.swift; sourceTree = "<group>"; };
B10047 /* FeedManagementSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedManagementSheet.swift; sourceTree = "<group>"; };
B10048 /* ScrollViewDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewDriver.swift; sourceTree = "<group>"; };
C10001 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D10001 /* Platform.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Platform.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@@ -336,6 +338,7 @@
B10045 /* ArticleView.swift */,
B10046 /* ArticleWebView.swift */,
B10047 /* FeedManagementSheet.swift */,
B10048 /* ScrollViewDriver.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -454,6 +457,7 @@
A10045 /* ArticleView.swift in Sources */,
A10046 /* ArticleWebView.swift in Sources */,
A10047 /* FeedManagementSheet.swift in Sources */,
A10048 /* ScrollViewDriver.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};