feat(reader): drive the reader to a passage via BibleReaderNavigation#174
Merged
Conversation
Contributor
✅ Commit Lint: passedAll commit messages in this PR conform to Conventional Commits. 📦 Release preview:
|
solomonstorts
force-pushed
the
ss/scroll-to-verse-within-chapter-BL-1901
branch
3 times, most recently
from
July 1, 2026 16:38
0fc1f61 to
1dcca45
Compare
Base automatically changed from
ss/scroll-to-verse-within-chapter-BL-1901
to
main
July 8, 2026 16:41
solomonstorts
force-pushed
the
ss/drive-reader-from-outside-BL-1901
branch
2 times, most recently
from
July 8, 2026 18:43
9277068 to
93e5c97
Compare
solomonstorts
marked this pull request as ready for review
July 8, 2026 18:56
solomonstorts
force-pushed
the
ss/drive-reader-from-outside-BL-1901
branch
from
July 8, 2026 19:55
e4798ad to
ca3c8ea
Compare
solomonstorts
force-pushed
the
ss/drive-reader-from-outside-BL-1901
branch
from
July 10, 2026 13:48
ca3c8ea to
2349da8
Compare
andrewse02
reviewed
Jul 10, 2026
andrewse02
left a comment
Member
There was a problem hiding this comment.
@solomonstorts I have a suggestion for an improved architecture, I think it makes the code a lot cleaner. It was easier to make the changes separately and open a PR to this branch as opposed to a ton of review comments. Would you mind taking a look at #190 and let me know what you think?
andrewse02
requested changes
Jul 10, 2026
andrewse02
approved these changes
Jul 10, 2026
bryanmontz
requested changes
Jul 13, 2026
bryanmontz
left a comment
Contributor
There was a problem hiding this comment.
Thanks, @solomonstorts. I left some comments and questions.
Contributor
|
@andrewse02 Please see my comments. |
solomonstorts
force-pushed
the
ss/drive-reader-from-outside-BL-1901
branch
3 times, most recently
from
July 13, 2026 18:38
47c632b to
44a2629
Compare
bryanmontz
requested changes
Jul 13, 2026
solomonstorts
force-pushed
the
ss/drive-reader-from-outside-BL-1901
branch
from
July 14, 2026 15:37
2665fbf to
c133834
Compare
bryanmontz
reviewed
Jul 16, 2026
solomonstorts
force-pushed
the
ss/drive-reader-from-outside-BL-1901
branch
2 times, most recently
from
July 16, 2026 13:45
c3442f2 to
afaed71
Compare
Add BibleReaderNavigation, a shared observable a host app uses to move an on-screen reader to a new passage from elsewhere — e.g. a "Read" button in another tab. Callers `request(reference, showsFullChapter:)`; the reader picks up the pending reference, loads that chapter in place via goToReference, and scrolls to the verse (building on the verse-scroll support from the prior commit). Adds the navigation init parameter to BibleReaderView, a Navigate tab to the sample app demonstrating it, and README docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A reader restored from storage (no explicit reference) dropped its showsFullChapter intent, so a passage last viewed as a full chapter came back as just its verse range after a cold launch. - Persist showsFullChapter to UserDefaults alongside reference - Restore it on the cold-launch branch of init; no verse scroll is armed on restore, so a user who scrolled within the chapter is not pulled back to the saved verse - Add persistence tests and clear the new key in test teardown Co-authored-by: Claude (AI Assistant)
The BibleReaderView example used the old `navigation:` label; the parameter is `readerNavigation:`, so the snippet would not compile. Co-authored-by: Claude (AI Assistant)
A full-chapter verse navigation (goToReference) armed a verse scroll, which suppresses the chapter-top scroll by setting scrollToTop back to false in the same run-loop turn. SwiftUI never observed the transient scrollToTop = true, so the onChange handler that clears isChangingChapter never fired and the flag stayed true — wedging handleScroll and leaving the header unresponsive to scrolling. - Clear isChangingChapter from VerseScrollCoordinator when the pending scroll resolves (landed, abandoned, or timed out), the moment that actually ends the chapter change for a verse scroll - Consolidate the scroll teardown into a single clearScrollState() - Assert isChangingChapter stays engaged until the scroll settles Co-authored-by: Claude (AI Assistant)
goToReference committed showsFullChapter and armed the verse scroll around an await that can fail. On a cross-version navigation whose version fetch fails, onHeaderSelectionChange swallows the error and leaves the old reference in place, which caused three issues: the verse scroll armed against a stale reference, the new showsFullChapter value was committed (and persisted) despite the failed load, and isChangingChapter stayed true — permanently gating handleScroll and freezing the header chrome. - Reorder so onHeaderSelectionChange runs first; commit showsFullChapter and arm the scroll only once the requested chapter actually loaded - Reset isChangingChapter in the failure branch so the header stays responsive - Assert showsFullChapter and isChangingChapter in the failure test Co-authored-by: Claude (AI Assistant)
#190) * refactor(reader): model scroll intent as a single ScrollAction BL-1901 Replace the scrollToTop flag + scrollTargetReference pair with a single ScrollAction enum (.none/.top/.toVerse) as the source of truth, removing the same-run-loop coalescing hazard where scrollToTop=true was overwritten before SwiftUI observed it. Add finishChapterChange() as the single navigation exit and route the coordinator's clearScrollState() and goToReference's error path through it. Collapse the two scroll onChange handlers into one. No public API change. * refactor(reader): route scroll-to-top settle through finishChapterChange BL-1901
- Reword the ScrollAction doc comment to describe the type rather than the old scrollToTop/scrollTargetReference pattern it replaced - Rename the verse case from .toVerse to .reference - Say "reference" instead of "verse" in the scrollTargetReference doc Co-authored-by: Claude (AI Assistant)
…L-1901 BibleReaderView previously took an optional reference plus a showsFullChapter flag, and silently ignored the flag in favor of the persisted value whenever reference was nil. Replace that shape with two coherent entry points: - init(reference:...) now requires a non-optional reference, so a display mode can no longer be passed without the reference it describes - BibleReaderView.restoringLastPassage(...) is a self-describing factory that restores the last-viewed passage and its display mode (or falls back to John 1), with no showsFullChapter parameter to conflict with the restored state BibleReaderViewModel mirrors the split with two convenience inits delegating to a private designated init that keeps the original resolution logic. No behavior change; the sample app adopts the factory for its reader tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bryan's init split made BibleReaderView(reference:) non-optional, which would have broken existing callers who relied on the nil default (e.g. BibleReaderView(), BibleReaderView(onVerseTap:)). Restore source compatibility instead of shipping a breaking change: - Re-add the original init(reference: BibleReference? = nil, ...) as a deprecated overload that forwards to the designated init. Callers keep compiling; the deprecation message steers them to a non-optional reference or BibleReaderView.restoringLastPassage(). - Update README examples to use restoringLastPassage() for the restore case. Verified with swift-api-digester: no breaking changes against the shipped baseline, so this remains a minor release. The baseline file is refreshed separately in the post-release chore, per repo convention (e.g. commit 051a318 "chore: update api-baseline files after 5.3.0 release"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename clearsPendingRequest() to clearPendingRequestSetsItToNil() so the test name reads as an action and outcome, matching the sibling tests (requestSetsPendingRequest, requestDefaultsToVerseRange). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
solomonstorts
force-pushed
the
ss/drive-reader-from-outside-BL-1901
branch
from
July 17, 2026 13:48
afaed71 to
82b9d25
Compare
Contributor
Code Coverage ReportCoverage after merging ss/drive-reader-from-outside-BL-1901 into main will be
Coverage Report
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
bryanmontz
approved these changes
Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds
BibleReaderNavigation, a shared@Observablea host app uses to move an on-screen reader to a new passage from elsewhere in the app — e.g. a "Read" button in another tab. A caller doesnavigation.request(reference, showsFullChapter:); the reader picks up the pending request, loads that chapter in place viagoToReference, and (for a full chapter) scrolls to the verse. This builds on the verse-scroll support merged in #173.What's included:
BibleReaderNavigation— a public observable withrequest(_:showsFullChapter:)and apendingRequestthe reader observes. The reader need not be on screen whenrequestis called; it picks up the pending request when it appears. Read-and-clear is a singleclearPendingRequest()on the object, and the reader drains the request from the view model (not the view).BibleReaderViewinit split —init(reference:)now takes a required reference, and a newBibleReaderView.restoringLastPassage(...)factory restores the last-viewed passage (or John 1 the first time). This is the recommended shape going forward — ashowsFullChapterflag can no longer be silently overridden by the persisted value. The old optional-referenceinit(reference:)is retained as a deprecated overload, so existing callers keep compiling (see Breaking Changes).goToReference— a view-model navigation operation that loads the chapter, then (only once it actually loaded) commits the display mode and arms the scroll. Lives alongside the othergoTo*methods.ScrollActionmodel — navigation intent is a singleScrollActionvalue (.none/.top/.reference) rather than a separatescrollToTopflag and scroll-target pair. This makes conflicting or empty scroll intent unrepresentable and removes the same-run-loop coalescing hazard that left the header chrome stuck after a verse navigation.showsFullChapteris now persisted and restored, so a passage last viewed as a full chapter comes back as a full chapter after the app is force-quit and relaunched (previously it came back as just the verse range). No verse scroll is armed on restore, so a user who had scrolled within the chapter isn't pulled back to the saved verse.restoringLastPassage()entry point.Type of Change
feat:New feature (non-breaking change which adds functionality)fix:Bug fix (non-breaking change which fixes an issue)docs:Documentation updaterefactor:Code refactoring (no functional changes)perf:Performance improvementtest:Test additions or updatesbuild:Build system or dependency changesci:CI configuration changeschore:Other changes (maintenance, etc.)This PR contains BREAKING CHANGES
No breaking changes. Bryan's init split originally made
BibleReaderView(reference:)non-optional, which would have broken existing callers relying on thenildefault (BibleReaderView(),BibleReaderView(onVerseTap:),BibleReaderView(showsFullChapter:)). To avoid that, the originalinit(reference: BibleReference? = nil, ...)is retained as a deprecated overload that forwards to the designated init; its deprecation message steers callers to pass a non-optional reference or useBibleReaderView.restoringLastPassage().Verified with
swift-api-digester: 0 breaking changes against the shippedv5.3.0baseline (raw, unfiltered). The public API baseline has been regenerated to the additive surface — the required-reference init, therestoringLastPassage()factory, andBibleReaderNavigation— with no removed symbols. This ships as a minor release (v5.4.0).Checklist
Conventional Commits
✅ All commits in this PR follow conventional commit format:
feat(reader): drive the reader to a passage via BibleReaderNavigationfix(reader): persist showsFullChapter across cold launch BL-1901docs(reader): fix navigation param label in README BL-1901fix(reader): release isChangingChapter after a verse scroll BL-1901fix(reader): handle failed chapter load in goToReference BL-1901refactor(reader): model scroll intent as a single ScrollAction BL-1901refactor(reader): split reader init into explicit and restore paths BL-1901docs(reader): keep BibleReaderView(reference:) source-compatible BL-1901test(reader): rename clear-request test to a verb phrase BL-1901Related Issues
Relates to BL-1901
Additional Context
Depends on / builds atop #173 (verse-scroll support), now merged to
main.Verified end-to-end in the sample app on the simulator:
Reviewer Notes
Incorporates review feedback from @bryanmontz:
init(reference:)required +restoringLastPassage()factory) resolves the "why persist / caller value overridden" thread: a display mode can no longer be passed without the reference it describes, and the restore path has noshowsFullChapterparameter to conflict with the restored state. The original optional-referenceinitis kept as a deprecated overload so no existing caller breaks —swift-api-digesterreports 0 breakages vs the shipped baseline.goToReferenceusesif/else(notguard) to route load-success vs. load-failure, andshowsFullChaptergained a default.viewModel.handleNavigationRequest(from:); the view just observespendingRequestand forwards. The parameter is a requiredBibleReaderNavigation(the nil-check lives at the call site).resetScrollStateForNewChapter()extracts the previously-duplicated chrome/scroll reset and replaces the hard-to-parse inline comment with an intent-first explanation.NavigationDemoView→NavigateViewin the sample app.Design notes:
ScrollAction— replaces thescrollToTopflag + scroll-target pair as the single source of truth for scroll intent.scrollTargetReferenceis now a computed projection of.reference, soVerseScrollCoordinatorreads it unchanged.finishChapterChange()is the one place a navigation returns the reader to rest.goToReferenceordering — loads the chapter first, then commitsshowsFullChapterand arms the scroll only if the requested reference actually loaded; the failure branch resetsisChangingChapterso the header stays responsive.Validation
Validated via the new page in the sample app:
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-07-08.at.14.09.05.mov
Greptile Summary
This PR introduces
BibleReaderNavigation, a shared@Observableclass that lets a host app drive the reader to a new passage from another tab without recreating it. It also refactors scroll intent into aScrollActionenum (fixing a coalescing hazard), persistsshowsFullChapteracross cold-launch, splitsBibleReaderView.initinto explicit and restore paths while keeping a deprecated overload for source compatibility, and addsgoToReferencefor cross-tab navigation.BibleReaderNavigationintroducesrequest(_:showsFullChapter:)/pendingRequest/clearPendingRequest()withonAppear+onChangeinReaderContentcovering both the "reader not yet on screen" and "reader already visible" cases.ScrollActionenum replaces thescrollToTopflag +scrollTargetReferencepair, making conflicting or empty scroll intent unrepresentable and resolving the same-run-loop coalescing hazard reported in previous review threads.goToReferenceroutes load-success vs. failure through anif/elsekeyed onreference == self.referencepost-await, fixing the silentshowsFullChapterpersistence and stuckisChangingChapterbugs from the previous review cycle.Confidence Score: 5/5
Safe to merge — the previous review's isChangingChapter-stuck and silent-persistence bugs are both resolved, and the new navigation flow is covered by tests.
The ScrollAction enum cleanly replaces the two-flag approach, goToReference correctly routes success vs. failure via reference == self.reference post-await (fixing both old stuck-chrome and premature-persist issues), and showsFullChapter is now only written after a confirmed successful load. The one open concern — an unstructured Task in handleNavigationRequest that can't be cancelled if a second request arrives mid-flight — is an unlikely edge case for cross-tab navigation and does not leave state permanently wedged.
No files require special attention. BibleReaderViewModel+Navigation.swift is the place to revisit if rapid concurrent navigation ever becomes a reported issue.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Host as Host App (NavigateView) participant Nav as BibleReaderNavigation participant View as ReaderContent (BibleReaderView) participant VM as BibleReaderViewModel participant VSC as VerseScrollCoordinator Host->>Nav: request(reference, showsFullChapter: true) Note over Nav: pendingRequest = Request(...) Host->>Host: "selectedTab = .bible" alt Reader already on screen Note over View: onChange(pendingRequest) fires else Reader just appeared Note over View: onAppear fires end View->>VM: handleNavigationRequest(from: nav) VM->>Nav: clearPendingRequest() Note over Nav: pendingRequest = nil VM->>VM: "Task { await goToReference(reference, showsFullChapter) }" VM->>VM: onHeaderSelectionChange(reference) Note over VM: isChangingChapter = true Note over VM: (load version if needed) Note over VM: self.reference = reference Note over VM: resetScrollStateForNewChapter() → scrollAction = .top alt "Load succeeded (reference == self.reference)" VM->>VM: "showsFullChapter = true (persisted)" VM->>VM: "setScrollTarget() → scrollAction = .reference(JHN 3:16)" View->>VSC: handleScrollTarget(proxy) VSC->>VSC: scroll to verse block VSC->>VM: "finishChapterChange() → isChangingChapter = false" else "Load failed (reference != self.reference)" VM->>VM: "finishChapterChange() → scrollAction = .none, isChangingChapter = false" end%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Host as Host App (NavigateView) participant Nav as BibleReaderNavigation participant View as ReaderContent (BibleReaderView) participant VM as BibleReaderViewModel participant VSC as VerseScrollCoordinator Host->>Nav: request(reference, showsFullChapter: true) Note over Nav: pendingRequest = Request(...) Host->>Host: "selectedTab = .bible" alt Reader already on screen Note over View: onChange(pendingRequest) fires else Reader just appeared Note over View: onAppear fires end View->>VM: handleNavigationRequest(from: nav) VM->>Nav: clearPendingRequest() Note over Nav: pendingRequest = nil VM->>VM: "Task { await goToReference(reference, showsFullChapter) }" VM->>VM: onHeaderSelectionChange(reference) Note over VM: isChangingChapter = true Note over VM: (load version if needed) Note over VM: self.reference = reference Note over VM: resetScrollStateForNewChapter() → scrollAction = .top alt "Load succeeded (reference == self.reference)" VM->>VM: "showsFullChapter = true (persisted)" VM->>VM: "setScrollTarget() → scrollAction = .reference(JHN 3:16)" View->>VSC: handleScrollTarget(proxy) VSC->>VSC: scroll to verse block VSC->>VM: "finishChapterChange() → isChangingChapter = false" else "Load failed (reference != self.reference)" VM->>VM: "finishChapterChange() → scrollAction = .none, isChangingChapter = false" endReviews (16): Last reviewed commit: "test(reader): rename clear-request test ..." | Re-trigger Greptile