Skip to content

a11y: label highlight color picker buttons and rendered highlights#198

Merged
joelbellyv merged 5 commits into
mainfrom
feat/highlights-a11y
Jul 16, 2026
Merged

a11y: label highlight color picker buttons and rendered highlights#198
joelbellyv merged 5 commits into
mainfrom
feat/highlights-a11y

Conversation

@joelbellyv

@joelbellyv joelbellyv commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds UI-automation accessibility metadata to two spots in the Bible reader (BibleReaderDrawer.swift, BibleTextView+Rendering.swift), plus a range-highlight lookup fix. Purely additive to the accessibility surface — nothing pre-existing was refactored.

Highlight state ships in .accessibilityIdentifier, not .accessibilityLabel / .accessibilityValue. Rationale: VoiceOver reads label / value (spoken to real users) but never reads identifier (silent, automation-only). Putting highlight-color announcements in the spoken attributes would make VoiceOver users hear a comma-joined summary of every highlight in a block before the scripture — bad UX for assistive-tech users. The Bible iOS app's own model (NativeReaderHost.swift) treats highlights as visual affordances only, never narrated. We match that: real users hear scripture; UI automation queries the identifier via Appium's @name fallback on iOS.

Color picker buttons (BibleReaderDrawer.swift)

  • Add-color: .accessibilityIdentifier("<Color> highlight")
  • Remove-color (checkmark overlay state): .accessibilityIdentifier("Remove <Color> highlight")
  • No .accessibilityLabel set — VoiceOver falls back to defaults (a follow-up ticket, YPE-4021, tracks adding localized user-facing labels once davidfedor confirms the wording).

Rendered verse text (BibleTextView+Rendering.swift)

  • Per-block Text carries .accessibilityIdentifier(highlightSummary(for: string)) — a comma-joined "verse N highlighted <color>" per highlighted verse in the block.
  • Verse's natural .accessibilityLabel (the scripture text from the SwiftUI Text) is unchanged — that's what VoiceOver reads.

Range highlight lookup fix (highlightFor(reference:))

  • Previously matched only when highlight.verseStart == reference.verseStart — a stored highlight covering v1–v5 announced only v1.
  • Now matches when the rendered run's verse falls in [verseStart, verseEnd] inclusive.

Color-name source

  • UIColor(color).accessibilityName (macOS: NSColor(color).accessibilityName), guarded via #if canImport(UIKit) / #elseif canImport(AppKit). Fixes macOS build regression.
  • Returns localized names on the user's device locale. Safe here because the string lands in .accessibilityIdentifier (silent, automation-only); target automation devices are English-only today. Non-English automation would need an internal hex→English mapping (deferred).

Automation contract change

The corpus (platform_swift_sdk_automation) queries these identifiers via @name. Previous locators used @label / @value; the new locators use @name because iOS Appium returns accessibilityIdentifier under @name (falling back to accessibilityLabel if identifier is unset). Corpus PR #22 updates the affected PageStore entries. Live-proven against BrowserStack real device: 42/43 flows pass, all 12 highlights flows green.

Test plan

  • Build succeeds locally (Release archive for iOS device).
  • macOS build compiles (no UIColor reference outside the #if canImport(UIKit) guard).
  • Physical iPhone install (development-signed, team AM88PVBRSG) verified via xcrun devicectl device install app.
  • VoiceOver — verses: swipe through a highlighted chapter with VoiceOver on. Confirmed VoiceOver reads scripture only; no "highlighted " announcement per verse. Highlights are silent to VoiceOver as intended.
  • VoiceOver — buttons: swipe to the color picker with VoiceOver on. Confirmed no "cyan highlight" / "yellow highlight" narration is spoken; VoiceOver falls back to iOS defaults ("button" on add-color, "highlight_checkmark button" on remove-color from the image asset name). This is intentional for this PR; localized user-facing labels are tracked in YPE-4021 as a davidfedor follow-up.
  • Appium tree — verses: confirmed XCUIElementTypeStaticText now surfaces the highlight summary under @name (from .accessibilityIdentifier); @value no longer carries the summary; @label still carries the scripture text.
  • Appium tree — buttons: confirmed XCUIElementTypeButton @name = "<Color> highlight" / "Remove <Color> highlight".
  • Range highlight: applied a multi-verse highlight (verses 1–5) and confirmed all five verses report their color in the identifier — not just verse 1.
  • End-to-end BS regression: full corpus run against bs://947d661e698ae9baefa844cd68d9a822a5adccab → 42/43 flows passed, 113/117 TCs (only 4 pre-existing untested_format from unrelated PageStore gaps; 1 flake version_detail_sample = BS session-init, not corpus).

Related follow-ups (tracked, not blocking this PR)

  • YPE-4021 — localized .accessibilityLabel on highlight color picker + remove-color buttons (addresses davidfedor's UX concern for real VoiceOver users; requires his input on wording + new String.localized keys).
  • YPE-4019 — canonical sign_in SharedStep with internal Path A/B branching (Codex design critique on corpus PR feat(reader): save and load remaining user settings e.g. fontFamily #22).
  • YPE-4020 — codify session lessons in hinqa standards + playbook docs (keyboard-dismiss pattern, .accessibilityLabel vs .accessibilityIdentifier, "passed=True ≠ intended effect").

Greptile Summary

This PR adds accessibility metadata for Bible reader highlights. The main changes are:

  • Color picker buttons now expose highlight action identifiers.
  • Rendered scripture blocks now expose highlight summaries through accessibility identifiers.
  • Highlight lookup now matches verses inside stored highlight ranges.
  • Color names now use guarded UIKit/AppKit paths for Apple platforms.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
Sources/YouVersionPlatformReader/Components/BibleReaderDrawer.swift Adds guarded UIKit/AppKit color-name lookup and highlight button accessibility identifiers.
Sources/YouVersionPlatformUI/Views/BibleTextView+Rendering.swift Adds rendered highlight identifiers and updates highlight matching to include verse ranges.

Reviews (4): Last reviewed commit: "style: split return nil to new line (Swi..." | Re-trigger Greptile

Context used:

  • Rule used - No hardcoded user-facing UI strings: use String.lo... (source)

Adds VoiceOver / UI-automation accessibility metadata to two spots in
the Bible reader:

1. Highlight color picker buttons — each add-state button gets
   `.accessibilityLabel("<Color> highlight")` and each remove-state
   (with checkmark overlay) button gets `.accessibilityLabel("Remove
   <Color> highlight")`. Reads the color live from the existing
   `highlightColors` array — no palette hardcoded on our side.

2. Rendered verse text — the per-block Text carries an
   `.accessibilityValue("verse N highlighted <color>")` per
   highlighted verse in the block, comma-joined. Reads the color live
   from `highlightFor(reference:)` on the actual server-stored
   highlight — no palette hardcoded.

Color names come from UIColor(color).accessibilityName, so:
* Any RGB value the user has (palette or custom) gets a real name.
* Names are localized by iOS to the user's device language
  ("orange" in en, "naranja" in es, etc.).
* Zero maintenance if the palette grows.

Additive metadata only — no behavior change beyond the (correct)
VoiceOver announcements. Nothing pre-existing was refactored.
@github-actions

github-actions Bot commented Jul 14, 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
[analyzer] Analyzing commit: feat(a11y): label highlight color picker buttons and rendered highlights

Adds VoiceOver / UI-automation accessibility metadata to two spots in
the Bible reader:

1. Highlight color picker buttons — each add-state button gets
   `.accessibilityLabel("<Color> highlight")` and each remove-state
   (with checkmark overlay) button gets `.accessibilityLabel("Remove
   <Color> highlight")`. Reads the color live from the existing
   `highlightColors` array — no palette hardcoded on our side.

2. Rendered verse text — the per-block Text carries an
   `.accessibilityValue("verse N highlighted <color>")` per
   highlighted verse in the block, comma-joined. Reads the color live
   from `highlightFor(reference:)` on the actual server-stored
   highlight — no palette hardcoded.

Color names come from UIColor(color).accessibilityName, so:
* Any RGB value the user has (palette or custom) gets a real name.
* Names are localized by iOS to the user's device language
  ("orange" in en, "naranja" in es, etc.).
* Zero maintenance if the palette grows.

Additive metadata only — no behavior change beyond the (correct)
VoiceOver announcements. Nothing pre-existing was refactored.
[analyzer] The release type for the commit is minor
[analyzer] Analyzing commit: style: implicit return in accessibilityColorName (SwiftLint)
[analyzer] The commit should not trigger a release
[analyzer] Analyzing commit: fix(a11y): move highlight state to accessibilityIdentifier + fix range + macOS build

Addresses PR #198 review feedback from davidfedor (CHANGES_REQUESTED)
and greptile (2 P1 findings).

1. VoiceOver UX (davidfedor): the previous shape put a concatenated
   summary of every highlight in the block onto `.accessibilityValue`,
   which VoiceOver reads. A user focusing on a paragraph would hear
   "verse 1 highlighted green, verse 2 highlighted light vibrant
   yellow, verse 3 highlighted light vibrant yellow, verse 4
   highlighted cyan, verse 5 highlighted cyan" before the scripture
   itself. That's noise for real assistive-tech users.

   The Bible iOS app's model (NativeReaderHost.swift:1648-1652 in
   the bible-ios repo) is per-verse a11y elements whose label is
   just "<verseNumber> <verseBody>" — highlights are visual only,
   never narrated inline. Following that: route the color-name
   string into `.accessibilityIdentifier` instead of
   `.accessibilityValue`. `accessibilityIdentifier` is
   UI-automation-only — VoiceOver never reads it, so real users hear
   scripture only, while Appium can still query per-verse highlight
   state via `@name` fallback on XCUIElementTypeStaticText. Same
   change on the color picker buttons in BibleReaderDrawer
   (`.accessibilityLabel` → `.accessibilityIdentifier`).

2. macOS build (greptile P1): UIColor is UIKit-only, unguarded in a
   package that declares macOS as a supported platform. Wrapped both
   accessibilityColorName helpers in
   `#if canImport(UIKit) / #elseif canImport(AppKit)` per
   davidfedor's suggested fix, using NSColor on macOS. Added the
   matching conditional imports at the top of each file. Needs
   macOS smoke to confirm NSColor.accessibilityName returns
   comparable strings — no functional dependency in the SDK code
   itself since the value is now identifier-only.

3. Range highlights (greptile P1): highlightFor(reference:) matched
   only when the stored highlight's verseStart equaled the run's
   verse. So a stored highlight covering v1-v5 announced only v1
   and dropped v2-v5 from the identifier. Fixed to match inclusion
   in [verseStart, verseEnd], with `verseEnd ?? verseStart` fallback
   for single-verse highlights.

No hardcoded palette map — accessibilityName still handles every
color the SDK ever ships (palette + custom), and since the string
is no longer spoken, the localized name is acceptable data for the
identifier.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[analyzer] The release type for the commit is patch
[analyzer] Analyzing commit: Merge remote-tracking branch 'origin/main' into feat/highlights-a11y

# Conflicts:
#	Sources/YouVersionPlatformUI/Views/BibleTextView+Rendering.swift
[analyzer] The commit should not trigger a release
[analyzer] Analyzing commit: style: split return nil to new line (SwiftLint conditional_returns_on_newline)

The `guard let refVerse = reference.verseStart else { return nil }`
one-liner added in the merge-resolution commit tripped SwiftLint's
conditional_returns_on_newline rule. Splitting the guard body onto
its own line.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[analyzer] The commit should not trigger a release
[analyzer] Analysis of 5 commits complete: minor release

Comment thread Sources/YouVersionPlatformReader/Components/BibleReaderDrawer.swift Outdated
Comment thread Sources/YouVersionPlatformUI/Views/BibleTextView+Rendering.swift

@davidfedor davidfedor 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.

There are a lot of improvements we'd need here, to make this a good user experience for non-developers using assistive tech.

Alternatively, you could consider only adding these labels on debug builds, if you're testing on debug builds. That'd dodge most of the complexity here.

Comment thread Sources/YouVersionPlatformReader/Components/BibleReaderDrawer.swift Outdated
Comment thread Sources/YouVersionPlatformReader/Components/BibleReaderDrawer.swift Outdated
Comment thread Sources/YouVersionPlatformReader/Components/BibleReaderDrawer.swift Outdated
Comment thread Sources/YouVersionPlatformReader/Components/BibleReaderDrawer.swift Outdated
Comment thread Sources/YouVersionPlatformUI/Views/BibleTextView+Rendering.swift Outdated
Comment thread Sources/YouVersionPlatformUI/Views/BibleTextView+Rendering.swift
Joel Bell and others added 3 commits July 15, 2026 17:38
…e + macOS build

Addresses PR #198 review feedback from davidfedor (CHANGES_REQUESTED)
and greptile (2 P1 findings).

1. VoiceOver UX (davidfedor): the previous shape put a concatenated
   summary of every highlight in the block onto `.accessibilityValue`,
   which VoiceOver reads. A user focusing on a paragraph would hear
   "verse 1 highlighted green, verse 2 highlighted light vibrant
   yellow, verse 3 highlighted light vibrant yellow, verse 4
   highlighted cyan, verse 5 highlighted cyan" before the scripture
   itself. That's noise for real assistive-tech users.

   The Bible iOS app's model (NativeReaderHost.swift:1648-1652 in
   the bible-ios repo) is per-verse a11y elements whose label is
   just "<verseNumber> <verseBody>" — highlights are visual only,
   never narrated inline. Following that: route the color-name
   string into `.accessibilityIdentifier` instead of
   `.accessibilityValue`. `accessibilityIdentifier` is
   UI-automation-only — VoiceOver never reads it, so real users hear
   scripture only, while Appium can still query per-verse highlight
   state via `@name` fallback on XCUIElementTypeStaticText. Same
   change on the color picker buttons in BibleReaderDrawer
   (`.accessibilityLabel` → `.accessibilityIdentifier`).

2. macOS build (greptile P1): UIColor is UIKit-only, unguarded in a
   package that declares macOS as a supported platform. Wrapped both
   accessibilityColorName helpers in
   `#if canImport(UIKit) / #elseif canImport(AppKit)` per
   davidfedor's suggested fix, using NSColor on macOS. Added the
   matching conditional imports at the top of each file. Needs
   macOS smoke to confirm NSColor.accessibilityName returns
   comparable strings — no functional dependency in the SDK code
   itself since the value is now identifier-only.

3. Range highlights (greptile P1): highlightFor(reference:) matched
   only when the stored highlight's verseStart equaled the run's
   verse. So a stored highlight covering v1-v5 announced only v1
   and dropped v2-v5 from the identifier. Fixed to match inclusion
   in [verseStart, verseEnd], with `verseEnd ?? verseStart` fallback
   for single-verse highlights.

No hardcoded palette map — accessibilityName still handles every
color the SDK ever ships (palette + custom), and since the string
is no longer spoken, the localized name is acceptable data for the
identifier.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts:
#	Sources/YouVersionPlatformUI/Views/BibleTextView+Rendering.swift
…_newline)

The `guard let refVerse = reference.verseStart else { return nil }`
one-liner added in the merge-resolution commit tripped SwiftLint's
conditional_returns_on_newline rule. Splitting the guard body onto
its own line.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Code Coverage Report

Coverage after merging feat/highlights-a11y 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%
   

@joelbellyv
joelbellyv merged commit bedcf60 into main Jul 16, 2026
10 checks passed
@joelbellyv
joelbellyv deleted the feat/highlights-a11y branch July 16, 2026 20:47
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.

2 participants