Skip to content

feat(reader): drive the reader to a passage via BibleReaderNavigation#174

Merged
solomonstorts merged 11 commits into
mainfrom
ss/drive-reader-from-outside-BL-1901
Jul 17, 2026
Merged

feat(reader): drive the reader to a passage via BibleReaderNavigation#174
solomonstorts merged 11 commits into
mainfrom
ss/drive-reader-from-outside-BL-1901

Conversation

@solomonstorts

@solomonstorts solomonstorts commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Description

Adds BibleReaderNavigation, a shared @Observable a 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 does navigation.request(reference, showsFullChapter:); the reader picks up the pending request, loads that chapter in place via goToReference, 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 with request(_:showsFullChapter:) and a pendingRequest the reader observes. The reader need not be on screen when request is called; it picks up the pending request when it appears. Read-and-clear is a single clearPendingRequest() on the object, and the reader drains the request from the view model (not the view).
  • BibleReaderView init splitinit(reference:) now takes a required reference, and a new BibleReaderView.restoringLastPassage(...) factory restores the last-viewed passage (or John 1 the first time). This is the recommended shape going forward — a showsFullChapter flag can no longer be silently overridden by the persisted value. The old optional-reference init(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 other goTo* methods.
  • ScrollAction model — navigation intent is a single ScrollAction value (.none / .top / .reference) rather than a separate scrollToTop flag 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.
  • Cold-launch fixshowsFullChapter is 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.
  • Failed-load handling — a cross-version navigation whose version fetch fails no longer commits the new display mode, arms a stale scroll, or leaves the header wedged.
  • Sample app — a "Navigate" tab demonstrating driving the reader from another tab.
  • README — docs for the navigation API and the 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 update

  • refactor: Code refactoring (no functional changes)

  • perf: Performance improvement

  • test: Test additions or updates

  • build: Build system or dependency changes

  • ci: CI configuration changes

  • chore: 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 the nil default (BibleReaderView(), BibleReaderView(onVerseTap:), BibleReaderView(showsFullChapter:)). To avoid that, the original init(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 use BibleReaderView.restoringLastPassage().

Verified with swift-api-digester: 0 breaking changes against the shipped v5.3.0 baseline (raw, unfiltered). The public API baseline has been regenerated to the additive surface — the required-reference init, the restoringLastPassage() factory, and BibleReaderNavigation — with no removed symbols. This ships as a minor release (v5.4.0).

Checklist

  • My code follows the project's code style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • All commit messages follow conventional commits format

Conventional Commits

All commits in this PR follow conventional commit format:

  • feat(reader): drive the reader to a passage via BibleReaderNavigation
  • fix(reader): persist showsFullChapter across cold launch BL-1901
  • docs(reader): fix navigation param label in README BL-1901
  • fix(reader): release isChangingChapter after a verse scroll BL-1901
  • fix(reader): handle failed chapter load in goToReference BL-1901
  • refactor(reader): model scroll intent as a single ScrollAction BL-1901
  • refactor(reader): split reader init into explicit and restore paths BL-1901
  • docs(reader): keep BibleReaderView(reference:) source-compatible BL-1901
  • test(reader): rename clear-request test to a verb phrase BL-1901

Related 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:

  • Navigate to John 3:16 (full chapter) → background → reopen → still shows the full chapter ✅
  • Navigate to John 3:16 (full chapter) → force-quit → cold launch → shows the full chapter (previously regressed to the verse range) ✅
  • After a full-chapter verse navigation, the header still hides/shows on scroll (no stuck chrome) ✅

Reviewer Notes

Incorporates review feedback from @bryanmontz:

  • Init split (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 no showsFullChapter parameter to conflict with the restored state. The original optional-reference init is kept as a deprecated overload so no existing caller breaks — swift-api-digester reports 0 breakages vs the shipped baseline.
  • goToReference uses if/else (not guard) to route load-success vs. load-failure, and showsFullChapter gained a default.
  • Navigation handling moved out of the view into viewModel.handleNavigationRequest(from:); the view just observes pendingRequest and forwards. The parameter is a required BibleReaderNavigation (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.
  • NavigationDemoViewNavigateView in the sample app.

Design notes:

  • ScrollAction — replaces the scrollToTop flag + scroll-target pair as the single source of truth for scroll intent. scrollTargetReference is now a computed projection of .reference, so VerseScrollCoordinator reads it unchanged. finishChapterChange() is the one place a navigation returns the reader to rest.
  • goToReference ordering — loads the chapter first, then commits showsFullChapter and arms the scroll only if the requested reference actually loaded; the failure branch resets isChangingChapter so 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 @Observable class that lets a host app drive the reader to a new passage from another tab without recreating it. It also refactors scroll intent into a ScrollAction enum (fixing a coalescing hazard), persists showsFullChapter across cold-launch, splits BibleReaderView.init into explicit and restore paths while keeping a deprecated overload for source compatibility, and adds goToReference for cross-tab navigation.

  • BibleReaderNavigation introduces request(_:showsFullChapter:) / pendingRequest / clearPendingRequest() with onAppear + onChange in ReaderContent covering both the "reader not yet on screen" and "reader already visible" cases.
  • ScrollAction enum replaces the scrollToTop flag + scrollTargetReference pair, making conflicting or empty scroll intent unrepresentable and resolving the same-run-loop coalescing hazard reported in previous review threads.
  • goToReference routes load-success vs. failure through an if/else keyed on reference == self.reference post-await, fixing the silent showsFullChapter persistence and stuck isChangingChapter bugs 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

Filename Overview
Sources/YouVersionPlatformReader/BibleReaderNavigation.swift New public observable class providing request(_:showsFullChapter:) and pendingRequest. Clean design; clearPendingRequest() is correctly internal-only.
Sources/YouVersionPlatformReader/ViewModels/BibleReaderViewModel+Navigation.swift Adds handleNavigationRequest and goToReference. The unstructured Task in handleNavigationRequest has no cancellation, so rapid concurrent navigations can race; otherwise the success/failure routing via reference == self.reference is correct.
Sources/YouVersionPlatformReader/ViewModels/BibleReaderViewModel.swift Replaces scrollToTop + scrollTargetReference with ScrollAction enum; adds showsFullChapter persistence and the finishChapterChange/resetScrollStateForNewChapter helpers. Logic is sound.
Sources/YouVersionPlatformReader/BibleReaderView.swift Splits init into explicit init(reference:), restoringLastPassage() factory, and deprecated optional-reference overload. Navigation handling in ReaderContent correctly uses both onAppear and onChange.
Sources/YouVersionPlatformReader/VerseScrollCoordinator.swift Extracts duplicated cleanup into clearScrollState() which now calls finishChapterChange() — consistent with the new lifecycle model.
Tests/YouVersionPlatformReaderTests/BibleReaderViewModelVerseScrollTests.swift Good coverage of goToReference paths: full-chapter verse scroll, verse-range (no scroll), failed cross-version load, and chapter-level reference. Also adds BibleReaderNavigationTests.
Tests/YouVersionPlatformReaderTests/BibleReaderViewModelPersistenceTests.swift Three new tests confirm showsFullChapter is restored from defaults when no explicit reference is given, ignored when an explicit reference is given, and persisted on assignment.
Tests/YouVersionPlatformReaderTests/BibleReaderViewModelTestSupport.swift Adds showsFullChapterKey constant and updates makeViewModel to branch on optional reference, matching the new designated init split.
Tests/YouVersionPlatformReaderTests/BibleReaderViewModel+NavigationTests.swift Updates assertions from scrollToTop == true/false to scrollAction == .top/.none — straightforward mechanical update, all assertions remain correct.
Tests/YouVersionPlatformReaderTests/BibleReaderViewModelNavigationStateTests.swift Single assertion updated from scrollToTop to scrollAction == .top. No logic change.
Examples/SampleApp/NavigateView.swift New sample-app view demonstrating the navigation API with four passage examples and a tab-switch callback.
Examples/SampleApp/SampleApp.swift Migrates BibleReaderView() to BibleReaderView.restoringLastPassage(readerNavigation:), adds the Navigate tab, and renumbers existing tab tags.
README.md Adds navigation API docs and restoringLastPassage() example. Code examples use readerNavigation: label consistently.

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
Loading
%%{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"
    end
Loading

Reviews (16): Last reviewed commit: "test(reader): rename clear-request test ..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

✅ Commit Lint: passed

All commit messages in this PR conform to Conventional Commits.

📦 Release preview: v5.3.0v5.4.0 (minor)

Merging this PR will, on the next manual release dispatch, ship as the version above.

Release analyzer output
bel; the
parameter is `readerNavigation:`, so the snippet would not compile.

Co-authored-by: Claude (AI Assistant)
[analyzer] The commit should not trigger a release
[analyzer] Analyzing commit: fix(reader): release isChangingChapter after a verse scroll BL-1901

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)
[analyzer] The release type for the commit is patch
[analyzer] Analyzing commit: fix(reader): handle failed chapter load in goToReference BL-1901

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)
[analyzer] The release type for the commit is patch
[analyzer] Analyzing commit: refactor(reader): model scroll intent as a single ScrollAction BL-1901 (#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
[analyzer] The commit should not trigger a release
[analyzer] Analyzing commit: refactor(reader): apply review feedback on ScrollAction 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)
[analyzer] The commit should not trigger a release
[analyzer] Analyzing commit: fix: address review comments
[analyzer] The release type for the commit is patch
[analyzer] Analyzing commit: refactor(reader): split reader init into explicit and restore paths BL-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>
[analyzer] The commit should not trigger a release
[analyzer] Analyzing commit: docs(reader): keep BibleReaderView(reference:) source-compatible BL-1901

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>
[analyzer] The commit should not trigger a release
[analyzer] Analyzing commit: test(reader): rename clear-request test to a verb phrase BL-1901

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>
[analyzer] The commit should not trigger a release
[analyzer] Analysis of 11 commits complete: minor release

@solomonstorts
solomonstorts force-pushed the ss/scroll-to-verse-within-chapter-BL-1901 branch 3 times, most recently from 0fc1f61 to 1dcca45 Compare July 1, 2026 16:38
Base automatically changed from ss/scroll-to-verse-within-chapter-BL-1901 to main July 8, 2026 16:41
@solomonstorts
solomonstorts force-pushed the ss/drive-reader-from-outside-BL-1901 branch 2 times, most recently from 9277068 to 93e5c97 Compare July 8, 2026 18:43
@solomonstorts
solomonstorts marked this pull request as ready for review July 8, 2026 18:56
Comment thread README.md Outdated
Comment thread Sources/YouVersionPlatformReader/ViewModels/BibleReaderViewModel+Navigation.swift Outdated
Comment thread Sources/YouVersionPlatformReader/ViewModels/BibleReaderViewModel+Navigation.swift Outdated
@solomonstorts
solomonstorts force-pushed the ss/drive-reader-from-outside-BL-1901 branch from e4798ad to ca3c8ea Compare July 8, 2026 19:55
@solomonstorts solomonstorts self-assigned this Jul 8, 2026
@solomonstorts
solomonstorts requested a review from andrewse02 July 8, 2026 20:47
@solomonstorts
solomonstorts force-pushed the ss/drive-reader-from-outside-BL-1901 branch from ca3c8ea to 2349da8 Compare July 10, 2026 13:48

@andrewse02 andrewse02 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Comment thread Sources/YouVersionPlatformReader/ViewModels/BibleReaderViewModel.swift Outdated
Comment thread Sources/YouVersionPlatformReader/ViewModels/BibleReaderViewModel.swift Outdated
Comment thread Sources/YouVersionPlatformReader/ViewModels/BibleReaderViewModel.swift Outdated
@solomonstorts
solomonstorts requested a review from andrewse02 July 10, 2026 20:33
@solomonstorts
solomonstorts requested a review from bryanmontz July 13, 2026 13:27

@bryanmontz bryanmontz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @solomonstorts. I left some comments and questions.

Comment thread Examples/SampleApp/NavigationDemoView.swift Outdated
Comment thread Sources/YouVersionPlatformReader/ViewModels/BibleReaderViewModel+Navigation.swift Outdated
Comment thread Sources/YouVersionPlatformReader/ViewModels/BibleReaderViewModel+Navigation.swift Outdated
Comment thread Sources/YouVersionPlatformReader/ViewModels/BibleReaderViewModel.swift Outdated
Comment thread Sources/YouVersionPlatformReader/BibleReaderView.swift
Comment thread Sources/YouVersionPlatformReader/BibleReaderView.swift Outdated
@bryanmontz

Copy link
Copy Markdown
Contributor

@andrewse02 Please see my comments.

@solomonstorts
solomonstorts force-pushed the ss/drive-reader-from-outside-BL-1901 branch 3 times, most recently from 47c632b to 44a2629 Compare July 13, 2026 18:38
@solomonstorts
solomonstorts requested a review from bryanmontz July 13, 2026 18:46
Comment thread Tests/YouVersionPlatformReaderTests/BibleReaderViewModelVerseScrollTests.swift Outdated
@solomonstorts solomonstorts changed the title feat(reader): drive the reader to a passage via BibleReaderNavigation feat(reader)!: drive the reader to a passage via BibleReaderNavigation Jul 14, 2026
Comment thread Sources/YouVersionPlatformReader/BibleReaderView.swift
@solomonstorts
solomonstorts force-pushed the ss/drive-reader-from-outside-BL-1901 branch from 2665fbf to c133834 Compare July 14, 2026 15:37
@solomonstorts solomonstorts changed the title feat(reader)!: drive the reader to a passage via BibleReaderNavigation feat(reader): drive the reader to a passage via BibleReaderNavigation Jul 14, 2026
@youversion youversion deleted a comment from github-actions Bot Jul 14, 2026
@solomonstorts
solomonstorts requested a review from bryanmontz July 15, 2026 14:02
Comment thread .api-baseline/YouVersionPlatformReader.json
@solomonstorts
solomonstorts force-pushed the ss/drive-reader-from-outside-BL-1901 branch 2 times, most recently from c3442f2 to afaed71 Compare July 16, 2026 13:45
@solomonstorts
solomonstorts requested a review from bryanmontz July 16, 2026 13:57
solomonstorts and others added 11 commits July 17, 2026 08:48
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
solomonstorts force-pushed the ss/drive-reader-from-outside-BL-1901 branch from afaed71 to 82b9d25 Compare July 17, 2026 13:48
@github-actions

Copy link
Copy Markdown
Contributor

Code Coverage Report

Coverage after merging ss/drive-reader-from-outside-BL-1901 into main will be
92.10%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/DerivedData/Build/Intermediates.noindex/YouVersionPlatform.build/Debug-iphonesimulator/YouVersionPlatformCoreTests.build/DerivedSources
   resource_bundle_accessor.swift87.50%100%75%88.89%19, 44–45
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore
   YouVersionPlatformConfiguration.swift90.99%100%80%92.71%147–149, 40–42
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs
   URLBuilder.swift88.70%100%90%88.54%110–119, 12–19
   URLRequest+YouVersion.swift100%100%100%100%
   YouVersionAPI.swift92.93%100%90%93.26%10–11, 75, 83, 85, 9
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/Bible
   BibleVersionAPI.swift73.50%100%66.67%74.51%118–132, 37–39, 58, 65–67, 75, 87
   BibleVersions.swift93.06%100%100%92.06%32, 54, 60–61, 83
   Highlights.swift77.71%100%59.09%80.39%109–116, 158, 161, 180, 183, 212, 215, 223, 227, 260–262, 63, 67, 79, 97
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/DataExchange
   DataExchange.swift97.37%100%100%97.14%39
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/Languages
   Languages.swift92.75%100%85.71%93.55%47–49, 79
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/Organizations
   Organizations.swift37.04%100%33.33%37.50%29–39, 41–43, 51
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/Users
   SignInWithYouVersionPKCEAuthorizationRequest.swift81.25%100%73.68%82.40%100, 111, 122–129, 137, 75–81, 99
   SignInWithYouVersionPermission.swift69.77%100%60%71.05%18, 20, 22, 26, 39, 50–51, 53–56
   SignInWithYouVersionResult.swift39.68%100%20%43.40%15–17, 44–56, 83–91
   Users.swift74.13%100%67.74%75%112, 150–156, 16, 161, 17, 171, 18–20, 208, 21, 211, 228, 234, 252–254, 278–280, 282–284, 286–288, 290–292, 297–299, 31–40, 44, 61, 67–71, 85
   YouVersionUserInfo.swift70.59%100%50%73.33%18, 29–31
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/VOTD
   VOTD.swift94.12%100%100%93.33%22
   YouVersionVerseOfTheDay.swift0%100%0%0%15–18, 20–25
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/Bible
   BibleBook.swift100%100%100%100%
   BibleChapterRepository.swift85.26%100%82.76%85.83%111–113, 42, 49, 54–57, 65–68, 83
   BibleContentStorage.swift100%100%100%100%
   BibleHighlight.swift55.56%100%50%57.14%12–14
   BibleHighlightsCache.swift86.71%100%83.33%87.61%103–105, 117–118, 62, 65–67, 72
   BibleHighlightsRepository.swift77.78%100%69.23%78.98%144–148, 19–21, 220–221, 223–227, 23–25, 253–254, 256–260, 27–29, 31–32, 328–329, 33, 330–354, 381–383, 407–411
   BibleHighlightsViewModel.swift90.68%100%85.71%91.43%33–36, 66, 69, 73
   BibleReference.swift83.37%100%66.10%86.10%136, 231–232, 256, 260, 293, 306, 316, 326, 336, 338–349, 53–57, 67–69, 88, 90, 92
   BibleTextNode.swift92.86%100%75%95.83%
   BibleTextNodeParser.swift94.29%100%100%93.79%104, 150, 155, 173, 190, 44, 56–59
   BibleVersion.swift70.10%100%86.67%67.07%107–126, 61, 71–74, 80
   BibleVersionRepository.swift86.30%100%83.02%87.03%125–128, 173, 212–217, 243, 248, 276–278, 71–74, 82–85
   DeprecatedBibleAPI.swift0%100%0%0%100, 102–104, 106–108, 110–112, 114–116, 119–121, 126, 128, 131–133, 138, 140, 142–144, 147–149, 27, 29–31, 36, 38, 40–42, 44–46, 48–50, 52–54, 56–58, 65–67, 69–71, 73–75, 77–79, 81–83, 85–87, 94–96, 98–99
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/Logging
   YouVersionPlatformLogger.swift62.50%100%50%65.91%100, 56–58, 77–79, 90, 92, 94, 98
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/Utilities
   AbbreviationSplitting.swift100%100%100%100%
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Tests/YouVersionPlatformCoreTests
   AbbreviationSplittingTests.swift100%100%100%100%
   BibleChapterRepositoryTests.swift98.68%100%94.12%99.25%
   BibleContentStorageTests.swift100%100%100%100%
   BibleHighlightsCacheTests.swift100%100%100%100%
   BibleHighlightsRepositoryTests.swift99.63%100%100%99.60%77, 87
   BibleHighlightsViewModelTests.swift94.06%100%92.11%94.34%19–25, 30, 38, 46, 85–87, 89–90
   BibleMetadataAPITests.swift100%100%100%100%
   BibleReferenceTests.swift100%100%100%100%
   BibleReferenceTests_adjacency.swift100%100%100%100%
   

@solomonstorts
solomonstorts merged commit 8b6382a into main Jul 17, 2026
10 checks passed
@solomonstorts
solomonstorts deleted the ss/drive-reader-from-outside-BL-1901 branch July 17, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants