Skip to content

iOS 26.5 fixes#171

Open
lukaskollmer wants to merge 5 commits into
mainfrom
lukas/ios-26.5-fixes-and-other-things
Open

iOS 26.5 fixes#171
lukaskollmer wants to merge 5 commits into
mainfrom
lukas/ios-26.5-fixes-and-other-things

Conversation

@lukaskollmer

@lukaskollmer lukaskollmer commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

iOS 26.5 fixes

♻️ Current situation & Problem

there are several SwiftUI issues/bugs in iOS 26.5, which cause certain sheets in MHC to get invalidated when they shouldn't (and consequently also dismissed when they shouldn't); this PR attempts to work around these issues.

⚙️ Release Notes

  • fixed: on iOS 26.5, the "Complete Demographics" sheet would get dismissed the second the user tried to interact with it
  • fixed: on iOS 26.5, the "Suggested Actions" sheet would get invalidated and reloaded when it shouldn't have
  • fixed: on iOS 26.5, when compiling with Xcode 26.6, the root TabView's Tabs would randomly have their accessibility identifiers ignored, causing issues with the UI tests

📚 Documentation

n/a

✅ Testing

the CI is currently is still using Xcode 26.5, i think, but i verified the tests locally using Xcode 26.6, and it is working fine there now, while it was consistently failing (due to the missing Tab accessibility identifiers) before these changes.

Code of Conduct & Contributing Guidelines

By creating and submitting this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features
    • Declined prompt suggestions now remain accessible from Settings.
  • Bug Fixes
    • Improved demographics saving when leaving the form to reduce missed updates.
    • Made sheet presentation more consistent across tab-based flows (demographics, account, prompted actions, and dashboard), preventing occasional state resets and restoring reliable navigation/sheet behavior.

attempts to work around FB23536382 and FB23551259
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds root-anchored adaptive sheet routing, flushes pending demographics writes on dismissal, refactors prompted-actions presentation, and replaces boolean test-login flags with structured credentials across setup, app startup, and UI tests.

Changes

Adaptive Sheet Routing and Presentation Updates

Layer / File(s) Summary
Root sheet router and adaptive sheet modifiers
MyHeartCounts/Utils/SwiftUI/RootSheetRouting.swift
Adds quirk detection, a root-anchored sheet router, the anchor modifier, and adaptive sheet overloads for boolean and item bindings.
Root anchor installation and sheet call-site updates
MyHeartCounts/Root View/RootView.swift, MyHeartCounts/Account/Demographics/DemographicsButton.swift, MyHeartCounts/Heart Health Dashboard/HeartHealthDashboard.swift, MyHeartCounts/Home Tab/Prompted Actions/PromptedActionsDigest.swift, MyHeartCounts/Root View/RootViewTab.swift
Installs the root sheet anchor and switches multiple sheet presentations to adaptiveSheet, while removing the dashboard article sheet branch.
Tab bar accessibility identifiers
MyHeartCounts/Utils/SwiftUI/TabBarAccessibilityFix.swift, MyHeartCounts/Root View/RootView.swift
Adds the tab-bar accessibility workaround and rewrites the tab view setup to use shared tab identifiers with that workaround.

Demographics Dismissal and Prompted Actions

Layer / File(s) Summary
Demographics flush and form dismissal
MyHeartCounts/Account/Demographics/DemographicsData.swift, MyHeartCounts/Account/Demographics/DemographicsForm.swift
Adds flush() to persist pending demographic writes immediately and calls it when the form disappears.
Demographics host completion tracking
MyHeartCounts/Home Tab/Prompted Actions/MHCPromptedActions.swift, MyHeartCounts/Resources/Localizable.xcstrings
Updates the demographics completion check, replaces the inline demographics host with a tracked DemographicsHost, and adjusts the prompted-actions footer text.
Prompted action sheet ownership
MyHeartCounts/Home Tab/Prompted Actions/PromptedActionsDigest.swift
Moves prompted-action sheet presentation to shared presentedAction state on the parent sheet and removes row-local sheet ownership.

Launch Options and UI Test Credentials

Layer / File(s) Summary
Structured login-and-enroll option
MyHeartCountsShared/Sources/MyHeartCountsShared/Launch Options/SetupTestEnvironmentConfig.swift
Replaces the boolean login flag with structured credentials and enum-based login options, and updates launch-option encoding/decoding.
Setup test environment login flow
MyHeartCounts/Modules/SetupTestEnvironment.swift
Switches setup-test-environment logic to the new login option type, passes credentials into login, and shortens the Firebase wait.
App startup and onboarding tests
MyHeartCounts/MyHeartCounts.swift, MyHeartCountsUITests/MHCTestCase.swift, MyHeartCountsUITests/AOnboardingTests.swift, MyHeartCountsUITests/BasicAppUsage.swift, MyHeartCountsUITests/OtherTests.swift, MyHeartCountsUITests/DemographicsNavigator.swift
Updates app startup defaults and onboarding/UI test flows to use structured credentials and .skip / .enable(.default) login settings, and adds a demographics navigator helper used by onboarding flow tests.
UI test helpers and prompted-action dismissal
MyHeartCountsUITests/UITestUtils.swift, MyHeartCountsUITests/PromptedActionsTests.swift, MyHeartCountsUITests/TabBarAXStatsTests.swift, MyHeartCountsUITests/MyHeartCounts UI Tests.xctestplan
Adds the sheet-expansion helper, updates the prompted-action dismissal test to use it, adds the tab-bar AX stats test, and skips that test in the test plan.

Accessibility Tree Dump Utility

Layer / File(s) Summary
AX dump traversal and formatting
MyHeartCounts/Utils/Debug Stuff/AXDump.swift
Adds runtime AX enabling, node classification, formatting, child enumeration, recursive dumping, and whole-app accessibility tree traversal.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: PSchmiedmayer

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is relevant, but it is too vague to identify the main change beyond generic iOS 26.5 fixes. Rename it to a specific summary of the primary change, such as the SwiftUI sheet routing and TabView accessibility fixes.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lukas/ios-26.5-fixes-and-other-things

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
MyHeartCounts/Modules/SetupTestEnvironment.swift (2)

154-168: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Signup fallback ignores the credentials parameter.

loginAndEnroll(_:) now takes a credentials parameter and uses it for the login attempt (lines 159-160), but the invalidCredentials signup fallback still hardcodes "leland@stanford.edu" / "StanfordRocks!" instead of credentials.username / credentials.password. If this is ever called with non-default credentials, login fails, then signup silently creates the default account instead of the requested one — defeating the purpose of accepting structured credentials.

🐛 Proposed fix
         } catch FirebaseAccountError.invalidCredentials {
             // account doesn't exist yet, signup
             var details = AccountDetails()
-            details.userId = "leland@stanford.edu"
-            details.password = "StanfordRocks!"
+            details.userId = credentials.username
+            details.password = credentials.password
             details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Modules/SetupTestEnvironment.swift` around lines 154 - 168, The
signup fallback in loginAndEnroll(_:) still hardcodes the default account values
instead of using the provided credentials parameter. Update the
FirebaseAccountError.invalidCredentials branch to populate AccountDetails from
credentials.username and credentials.password so the same requested account is
created after a failed login. Keep the existing login check and fallback
structure, but ensure all account creation fields in
SetupTestEnvironment.loginAndEnroll(_:) are driven by the passed-in credentials
rather than fixed literals.

75-78: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Firebase-load wait shortened from 4s to 1s.

Reducing the fixed sleep after Spezi.loadFirebase by 75% risks proceeding to setUp() before Firebase has finished initializing on slower CI runners, potentially causing intermittent failures in the login/enroll flow that immediately follows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Modules/SetupTestEnvironment.swift` around lines 75 - 78, The
Firebase initialization pause in SetupTestEnvironment is too short and can let
setUp() proceed before Spezi.loadFirebase has finished on slower runners. Update
the wait in the Firebase-loading block to use a safer synchronization approach
in place of the fixed 1-second sleep, or restore a longer delay if no readiness
signal exists, so the subsequent login/enroll flow starts only after Firebase is
ready.
🧹 Nitpick comments (1)
MyHeartCounts/Utils/SwiftUI/TabBarAccessibilityFix.swift (1)

52-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider logging when the retry budget is exhausted without success.

If apply() never returns true within the 40 attempts (10s), the task silently gives up and identifiers are never applied — since this workaround exists specifically to fix flaky UI-test tab lookups, a silent failure here could reintroduce the same flakiness this PR is meant to resolve, without any diagnostic trail.

♻️ Proposed diagnostic on exhaustion
             initialApplication = Task { `@MainActor` [weak self] in
                 for _ in 0..<40 { // the item views usually exist within a few hundred ms
                     guard let self, !Task.isCancelled else {
                         return
                     }
                     if self.apply() {
                         return
                     }
                     try? await Task.sleep(for: .milliseconds(250))
                 }
+                Logger.debugStuff.warning("TabBarAccessibilityFixer failed to resolve tab bar item views within retry budget")
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Utils/SwiftUI/TabBarAccessibilityFix.swift` around lines 52 -
62, The retry loop in TabBarAccessibilityFix’s initialApplication task currently
exits silently after exhausting all 40 attempts, so add a diagnostic when
apply() never succeeds. Update the Task block that repeatedly calls apply() to
detect the exhausted-retries case and log a clear failure message before
returning, keeping the existing cancellation and success paths unchanged. Use
the existing identifiers initialApplication and apply() to place the logging in
the right spot.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MyHeartCounts/Utils/Debug` Stuff/AXDump.swift:
- Around line 45-47: The AXDump classification logic in the button-traits branch
is too broad because .selected is causing non-switch controls to be labeled as
Switch. Update the conditional in AXDump.swift so only actual switch controls
identified by obj is UISwitch return "Switch", and keep all selected
buttons/tabs classified as "Button" in the traits.contains(.button) path.
- Around line 14-23: Gate the private AX runtime hook path in AXDump so it only
runs for debug/simulator builds; the current enableAXRuntime() call chain is
reachable from dumpAll() and can ship _AXSSetAutomationEnabled /
_AXSApplicationAccessibilitySetEnabled in Release. Add a DEBUG + simulator-only
compile guard around the AXDump helpers, or otherwise exclude AXDump from the
release app target, keeping the change localized to AXDump and dumpAll().

In `@MyHeartCountsShared/Sources/MyHeartCountsShared/Launch`
Options/SetupTestEnvironmentConfig.swift:
- Around line 104-109: The login-and-enroll launch argument in
SetupTestEnvironmentConfig is emitting plaintext credentials that later get
printed by MHCTestCase.launchAppAndEnrollIntoStudy, so adjust the
launch-argument construction to avoid exposing the password (and ideally the
username too) in any logged value. Update the handling in the loginAndEnroll
switch so the generated argument is sanitized or replaced with a non-sensitive
token, and keep the actual credentials only in memory for the code path that
needs them.
- Around line 104-109: The `loginAndEnroll` switch in
`SetupTestEnvironmentConfig` contains a redundant `let _ = ()` in the `.skip`
case that triggers SwiftLint. Remove the no-op binding and leave the `.skip`
branch empty, while keeping the `.enable(let credentials)` branch unchanged so
the switch remains valid and lint-clean.
- Around line 69-72: The validation in SetupTestEnvironmentConfig is checking
for the allowed reset and login-and-enroll tokens instead of invalid ones,
causing valid launch options to be rejected and ignored. Update the rawArgs
filtering logic in SetupTestEnvironmentConfig to detect non-allowed inputs by
comparing against allowedOptions, and keep the LaunchOptionDecodingError.other
path only for truly invalid values so _value(for:) can decode the intended
config.

---

Outside diff comments:
In `@MyHeartCounts/Modules/SetupTestEnvironment.swift`:
- Around line 154-168: The signup fallback in loginAndEnroll(_:) still hardcodes
the default account values instead of using the provided credentials parameter.
Update the FirebaseAccountError.invalidCredentials branch to populate
AccountDetails from credentials.username and credentials.password so the same
requested account is created after a failed login. Keep the existing login check
and fallback structure, but ensure all account creation fields in
SetupTestEnvironment.loginAndEnroll(_:) are driven by the passed-in credentials
rather than fixed literals.
- Around line 75-78: The Firebase initialization pause in SetupTestEnvironment
is too short and can let setUp() proceed before Spezi.loadFirebase has finished
on slower runners. Update the wait in the Firebase-loading block to use a safer
synchronization approach in place of the fixed 1-second sleep, or restore a
longer delay if no readiness signal exists, so the subsequent login/enroll flow
starts only after Firebase is ready.

---

Nitpick comments:
In `@MyHeartCounts/Utils/SwiftUI/TabBarAccessibilityFix.swift`:
- Around line 52-62: The retry loop in TabBarAccessibilityFix’s
initialApplication task currently exits silently after exhausting all 40
attempts, so add a diagnostic when apply() never succeeds. Update the Task block
that repeatedly calls apply() to detect the exhausted-retries case and log a
clear failure message before returning, keeping the existing cancellation and
success paths unchanged. Use the existing identifiers initialApplication and
apply() to place the logging in the right spot.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c22beca-b156-4742-9f88-4aa1458cdb32

📥 Commits

Reviewing files that changed from the base of the PR and between c194a63 and 08b2ee8.

📒 Files selected for processing (15)
  • MyHeartCounts/Modules/SetupTestEnvironment.swift
  • MyHeartCounts/MyHeartCounts.swift
  • MyHeartCounts/Root View/RootView.swift
  • MyHeartCounts/Utils/Debug Stuff/AXDump.swift
  • MyHeartCounts/Utils/SwiftUI/TabBarAccessibilityFix.swift
  • MyHeartCountsShared/Sources/MyHeartCountsShared/Launch Options/SetupTestEnvironmentConfig.swift
  • MyHeartCountsShared/Sources/MyHeartCountsShared/UITesting.swift
  • MyHeartCountsUITests/AOnboardingTests.swift
  • MyHeartCountsUITests/BasicAppUsage.swift
  • MyHeartCountsUITests/DemographicsNavigator.swift
  • MyHeartCountsUITests/MHCTestCase.swift
  • MyHeartCountsUITests/MyHeartCounts UI Tests.xctestplan
  • MyHeartCountsUITests/OtherTests.swift
  • MyHeartCountsUITests/PromptedActionsTests.swift
  • MyHeartCountsUITests/TabBarAXStatsTests.swift
💤 Files with no reviewable changes (1)
  • MyHeartCountsShared/Sources/MyHeartCountsShared/UITesting.swift
✅ Files skipped from review due to trivial changes (1)
  • MyHeartCountsUITests/MyHeartCounts UI Tests.xctestplan
🚧 Files skipped from review as they are similar to previous changes (1)
  • MyHeartCountsUITests/PromptedActionsTests.swift

Comment on lines +14 to +23
nonisolated static func enableAXRuntime() {
guard let lib = dlopen("/usr/lib/libAccessibility.dylib", RTLD_NOW) else {
return
}
typealias SetEnabled = @convention(c) (Bool) -> Void
for sym in ["_AXSSetAutomationEnabled", "_AXSApplicationAccessibilitySetEnabled"] {
if let ptr = dlsym(lib, sym) {
unsafeBitCast(ptr, to: SetEnabled.self)(true)
}
}

@coderabbitai coderabbitai Bot Jul 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether AXDump.swift is compiled into release app targets.
fd -a 'project.pbxproj$' . | xargs -r sed -n '/AXDump.swift/,+6p'

Repository: SchmiedmayerLab/MyHeartCounts-iOS

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and any project references to it.
git ls-files | rg -n '(^|/)AXDump\.swift$|project\.pbxproj$|\.xcodeproj/project\.pbxproj$'

# Show the file and surrounding context.
ax_file=$(git ls-files | rg '(^|/)AXDump\.swift$' | head -n1)
if [ -n "${ax_file:-}" ]; then
  printf '\n### %s\n' "$ax_file"
  cat -n "$ax_file" | sed -n '1,260p'
fi

# Show project references to the file and build-configuration clues.
for pbx in $(git ls-files | rg '(^|/)project\.pbxproj$'); do
  printf '\n### %s references\n' "$pbx"
  rg -n -C 3 'AXDump\.swift|DEBUG|targetEnvironment\(simulator\)|SWIFT_ACTIVE_COMPILATION_CONDITIONS|EXCLUDED_SOURCE_FILE_NAMES' "$pbx"
done

Repository: SchmiedmayerLab/MyHeartCounts-iOS

Length of output: 11151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

pbx="MyHeartCounts.xcodeproj/project.pbxproj"

# Show the file reference, build-file entry, and the app target's sources phase.
rg -n -C 4 'AXDump\.swift|PBXSourcesBuildPhase|Sources \*/ = \{|Files = \(|MyHeartCounts \*/ = \{|Release|DEBUG' "$pbx"

# Narrow to the source build phase entries that include AXDump.swift.
rg -n -C 2 'AXDump\.swift' "$pbx"

# Print the surrounding section where AXDump.swift is listed in the sources phase.
line=$(rg -n 'AXDump\.swift' "$pbx" | head -n1 | cut -d: -f1)
if [ -n "${line:-}" ]; then
  start=$((line-20)); [ "$start" -lt 1 ] && start=1
  end=$((line+20))
  sed -n "${start},${end}p" "$pbx"
fi

Repository: SchmiedmayerLab/MyHeartCounts-iOS

Length of output: 10319


🏁 Script executed:

#!/bin/bash
set -euo pipefail

pbx="MyHeartCounts.xcodeproj/project.pbxproj"

# Inspect the file-system-synchronized sections and any exception sets tied to the MyHeartCounts root group.
rg -n -C 4 'PBXFileSystemSynchronized(BuildFileExceptionSet|RootGroup)|80F72B(D7|E3)|AXDump\.swift|exceptions = \(' "$pbx"

# Print the section around the MyHeartCounts root group and its exception set.
start=$(rg -n '80F72BA52D7E008E00150FE3 /\* MyHeartCounts \*/' "$pbx" | head -n1 | cut -d: -f1)
if [ -n "${start:-}" ]; then
  sed -n "$((start-10)),$((start+40))p" "$pbx"
fi

# Print the exception set section near the referenced exception UUID from the root group.
ex=$(rg -n '80F72BD72D7E00A100150FE3 /\* PBXFileSystemSynchronizedBuildFileExceptionSet \*/' "$pbx" | head -n1 | cut -d: -f1 || true)
if [ -n "${ex:-}" ]; then
  sed -n "$((ex-10)),$((ex+40))p" "$pbx"
fi

Repository: SchmiedmayerLab/MyHeartCounts-iOS

Length of output: 7269


Gate these private AX runtime hooks to debug/simulator builds. MyHeartCounts/Utils/Debug Stuff/AXDump.swift is part of the app target, and dumpAll() always reaches enableAXRuntime(), so _AXSSetAutomationEnabled / _AXSApplicationAccessibilitySetEnabled can still ship in Release. Wrap this path in #if DEBUG && targetEnvironment(simulator) or exclude the file from the release target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Utils/Debug` Stuff/AXDump.swift around lines 14 - 23, Gate the
private AX runtime hook path in AXDump so it only runs for debug/simulator
builds; the current enableAXRuntime() call chain is reachable from dumpAll() and
can ship _AXSSetAutomationEnabled / _AXSApplicationAccessibilitySetEnabled in
Release. Add a DEBUG + simulator-only compile guard around the AXDump helpers,
or otherwise exclude AXDump from the release app target, keeping the change
localized to AXDump and dumpAll().

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Probably actually a good idea 👍

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@PSchmiedmayer Glad that helps! Here's a concrete way to gate it — wrap the risky entry points in AXDump.swift with a compile-time guard so the AX automation hooks never make it into Release/device builds:

`#if` DEBUG && targetEnvironment(simulator)
`@MainActor`
enum AXDump {
    nonisolated static func enableAXRuntime() {
        ...
    }
    ...
}
`#endif`

And guard the call site in dumpAll() (or wherever it's invoked) similarly, so callers don't fail to compile in Release:

`#if` DEBUG && targetEnvironment(simulator)
AXDump.dumpAll()
`#endif`

Alternatively, if you'd rather keep it usable on-device for internal debug builds too, #if DEBUG alone (without the simulator check) would still strip it from Release/TestFlight/App Store builds, which is the main risk here.

Let me know once it's in and I'll take another look.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@PSchmiedmayer no worries; that's the plan anyway. this is a debugging tool which either will be removed entirely, or will become a noop for non–debug builds

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +45 to +47
if traits.contains(.button) {
return traits.contains(.selected) || obj is UISwitch ? "Switch" : "Button"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don’t classify selected buttons as switches.

.selected is not switch semantics; selected tabs/buttons can be reported as Switch here. Restrict Switch to actual switch controls and keep selected buttons as Button.

Proposed fix
         let traits = obj.accessibilityTraits
         if traits.contains(.button) {
-            return traits.contains(.selected) || obj is UISwitch ? "Switch" : "Button"
+            return obj is UISwitch ? "Switch" : "Button"
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if traits.contains(.button) {
return traits.contains(.selected) || obj is UISwitch ? "Switch" : "Button"
}
if traits.contains(.button) {
return obj is UISwitch ? "Switch" : "Button"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Utils/Debug` Stuff/AXDump.swift around lines 45 - 47, The
AXDump classification logic in the button-traits branch is too broad because
.selected is causing non-switch controls to be labeled as Switch. Update the
conditional in AXDump.swift so only actual switch controls identified by obj is
UISwitch return "Switch", and keep all selected buttons/tabs classified as
"Button" in the traits.contains(.button) path.

Comment on lines +104 to 109
switch loginAndEnroll {
case .skip:
let _ = ()
case .enable(let credentials):
"\(Self.loginAndEnrollOptionName):\(credentials.username);\(credentials.password)"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Plaintext credentials embedded in launch args get printed to test logs.

login-and-enroll:<username>;<password> is emitted verbatim as a launch argument, and MHCTestCase.launchAppAndEnrollIntoStudy prints the full launchArguments list (including this value) to stdout/CI logs. For the hardcoded default test account this is low risk, but any custom Credentials passed to this API would leak in plaintext in CI logs.

🧰 Tools
🪛 GitHub Check: SwiftLint / Run SwiftLint

[failure] 106-106:
Redundant Discardable Let Violation: Prefer _ = foo() over let _ = foo() when discarding a result from a function (redundant_discardable_let)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCountsShared/Sources/MyHeartCountsShared/Launch`
Options/SetupTestEnvironmentConfig.swift around lines 104 - 109, The
login-and-enroll launch argument in SetupTestEnvironmentConfig is emitting
plaintext credentials that later get printed by
MHCTestCase.launchAppAndEnrollIntoStudy, so adjust the launch-argument
construction to avoid exposing the password (and ideally the username too) in
any logged value. Update the handling in the loginAndEnroll switch so the
generated argument is sanitized or replaced with a non-sensitive token, and keep
the actual credentials only in memory for the code path that needs them.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

SwiftLint: redundant let _ = ().

Static analysis flags this as a failing check.

🔧 Proposed fix
             switch loginAndEnroll {
             case .skip:
-                let _ = ()
+                _ = ()
             case .enable(let credentials):
                 "\(Self.loginAndEnrollOptionName):\(credentials.username);\(credentials.password)"
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch loginAndEnroll {
case .skip:
let _ = ()
case .enable(let credentials):
"\(Self.loginAndEnrollOptionName):\(credentials.username);\(credentials.password)"
}
switch loginAndEnroll {
case .skip:
_ = ()
case .enable(let credentials):
"\(Self.loginAndEnrollOptionName):\(credentials.username);\(credentials.password)"
}
🧰 Tools
🪛 GitHub Check: SwiftLint / Run SwiftLint

[failure] 106-106:
Redundant Discardable Let Violation: Prefer _ = foo() over let _ = foo() when discarding a result from a function (redundant_discardable_let)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCountsShared/Sources/MyHeartCountsShared/Launch`
Options/SetupTestEnvironmentConfig.swift around lines 104 - 109, The
`loginAndEnroll` switch in `SetupTestEnvironmentConfig` contains a redundant
`let _ = ()` in the `.skip` case that triggers SwiftLint. Remove the no-op
binding and leave the `.skip` branch empty, while keeping the `.enable(let
credentials)` branch unchanged so the switch remains valid and lint-clean.

Source: Linters/SAST tools

@PSchmiedmayer PSchmiedmayer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for the improvements here, good to also see some more stability for the tests 👍

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
MyHeartCountsUITests/DemographicsNavigator.swift (1)

99-106: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No existence wait before interacting with optionButton.

optionButton.isSelected is read immediately after the query is built, without a waitForExistence guard. If the sub-screen hasn't finished rendering, .isSelected defaults to false and triggers an unguarded .tap() on a possibly-missing element, risking flakiness. Other elements in this file gate interaction on waitForExistence first (e.g. Lines 35-36).

♻️ Suggested robustness fix
         app.staticTexts["Referral Source"].tap()
         let optionButton = app.buttons.matching("label CONTAINS[c] %@", title).firstMatch
+        XCTAssert(optionButton.waitForExistence(timeout: 2))
         if !optionButton.isSelected {
             optionButton.tap()
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCountsUITests/DemographicsNavigator.swift` around lines 99 - 106, The
fillInReferralSourceOption helper in DemographicsNavigator is interacting with
optionButton before confirming it exists. Add a waitForExistence guard on the
queried button before checking isSelected or tapping it, and only proceed once
the option is present, matching the existing wait-first pattern used elsewhere
in this file.
MyHeartCounts/Utils/AccountDetails+Diffing.swift (3)

23-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant nested try.

try decoder.decode(JSONObject.self, from: try encoder.encode(other)) — the inner try is unnecessary since the outer try already propagates the error; only one try is needed per throwing expression chain.

♻️ Cleanup
-            let jsonOld = try decoder.decode(JSONObject.self, from: try encoder.encode(other))
-            let jsonNew = try decoder.decode(JSONObject.self, from: try encoder.encode(self))
+            let jsonOld = try decoder.decode(JSONObject.self, from: encoder.encode(other))
+            let jsonNew = try decoder.decode(JSONObject.self, from: encoder.encode(self))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Utils/AccountDetails`+Diffing.swift around lines 23 - 24,
Remove the redundant nested try in the AccountDetails diffing logic by
simplifying the JSONObject decoding expressions in the
AccountDetails+Diffing.swift implementation. Update the code around the jsonOld
and jsonNew assignments so each throwing chain uses only the necessary try once,
while keeping the same encoder and decoder behavior in the diffing method.

19-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silent failure defeats the purpose of a debug helper.

The catch block swallows the error and returns "", so any encoding/decoding failure (e.g. an AccountKey value that fails to encode) produces an empty, indistinguishable-from-"no diff" result. Since this method exists purely for debugging, at minimum include the error in the returned string.

🐛 Proposed fix
         } catch {
-            return ""
+            return "Failed to compute diff: \(error)"
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Utils/AccountDetails`+Diffing.swift around lines 19 - 38, The
`AccountDetails.diff(from:)` debug helper is swallowing encoding/decoding
failures by returning an empty string from the `catch` block, which hides real
errors and makes failures indistinguishable from “no diff.” Update the `catch`
in `AccountDetails+Diffing` to return a string that includes the underlying
error details instead of `""`, so failures in the JSONEncoder/JSONDecoder path
are visible when diffing `AccountDetails` instances.

26-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tuple interpolation produces raw, unlabeled debug output.

entry for removed/inserted is a (Key, Value) tuple and mutated is (key:, old:, new:); default string interpolation of tuples yields output like ("email", JSONValue.string("x")), which is usable but not very readable for a debug diff. Consider formatting explicitly (e.g. "\(entry.0): \(entry.1)" and "\(mutated.key): \(mutated.old) -> \(mutated.new)").

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCounts/Utils/AccountDetails`+Diffing.swift around lines 26 - 34, The
debug diff formatting in AccountDetails+Diffing should not rely on default tuple
interpolation, since it prints unreadable raw tuple/debug text. Update the loop
in the diff-building logic to format each `removed` and `inserted` `(Key,
Value)` entry explicitly using its key and value, and format each `mutated` item
from the diff with its `key`, `old`, and `new` fields in a readable transition
form. Use the existing diff handling code around `removed`, `inserted`, and
`mutated` to make the output clearer.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MyHeartCountsUITests/DemographicsNavigator.swift`:
- Around line 99-106: `fillInReferralSourceOption(_:)` in
`DemographicsNavigator` is missing the same post-selection assertion used by the
other helper flows in `fillInDemographics()`. After tapping the referral source
option and navigating back, verify that the expected summary label/value is
shown again before returning, using the same pattern as the existing helper
methods so a failed tap is caught immediately.

---

Nitpick comments:
In `@MyHeartCounts/Utils/AccountDetails`+Diffing.swift:
- Around line 23-24: Remove the redundant nested try in the AccountDetails
diffing logic by simplifying the JSONObject decoding expressions in the
AccountDetails+Diffing.swift implementation. Update the code around the jsonOld
and jsonNew assignments so each throwing chain uses only the necessary try once,
while keeping the same encoder and decoder behavior in the diffing method.
- Around line 19-38: The `AccountDetails.diff(from:)` debug helper is swallowing
encoding/decoding failures by returning an empty string from the `catch` block,
which hides real errors and makes failures indistinguishable from “no diff.”
Update the `catch` in `AccountDetails+Diffing` to return a string that includes
the underlying error details instead of `""`, so failures in the
JSONEncoder/JSONDecoder path are visible when diffing `AccountDetails`
instances.
- Around line 26-34: The debug diff formatting in AccountDetails+Diffing should
not rely on default tuple interpolation, since it prints unreadable raw
tuple/debug text. Update the loop in the diff-building logic to format each
`removed` and `inserted` `(Key, Value)` entry explicitly using its key and
value, and format each `mutated` item from the diff with its `key`, `old`, and
`new` fields in a readable transition form. Use the existing diff handling code
around `removed`, `inserted`, and `mutated` to make the output clearer.

In `@MyHeartCountsUITests/DemographicsNavigator.swift`:
- Around line 99-106: The fillInReferralSourceOption helper in
DemographicsNavigator is interacting with optionButton before confirming it
exists. Add a waitForExistence guard on the queried button before checking
isSelected or tapping it, and only proceed once the option is present, matching
the existing wait-first pattern used elsewhere in this file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aaba5ae1-cff7-4c6c-8a83-ec45e26c8053

📥 Commits

Reviewing files that changed from the base of the PR and between 08b2ee8 and dbdb5bf.

📒 Files selected for processing (6)
  • MyHeartCounts/Home Tab/Prompted Actions/MHCPromptedActions.swift
  • MyHeartCounts/Utils/AccountDetails+Diffing.swift
  • MyHeartCountsShared/Sources/MyHeartCountsShared/Launch Options/SetupTestEnvironmentConfig.swift
  • MyHeartCountsShared/Sources/MyHeartCountsShared/Utils/JSONValue.swift
  • MyHeartCountsUITests/DemographicsNavigator.swift
  • MyHeartCountsUITests/PromptedActionsTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • MyHeartCountsShared/Sources/MyHeartCountsShared/Launch Options/SetupTestEnvironmentConfig.swift
  • MyHeartCountsUITests/PromptedActionsTests.swift

Comment on lines +99 to +106
func fillInReferralSourceOption(_ title: String) {
app.staticTexts["Referral Source"].tap()
let optionButton = app.buttons.matching("label CONTAINS[c] %@", title).firstMatch
if !optionButton.isSelected {
optionButton.tap()
}
app.navigationBars.buttons["BackButton"].tap()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing post-selection verification, unlike the rest of the navigator's helpers.

Every other selection flow in fillInDemographics() re-asserts the resulting summary label after returning from the sub-screen (e.g. Lines 63, 68, 74, 79, 85), but fillInReferralSourceOption does not verify that the option was actually selected before returning. If the tap silently fails to register, this test would not catch it.

✅ Proposed fix to verify selection after navigating back
     func fillInReferralSourceOption(_ title: String) {
         app.staticTexts["Referral Source"].tap()
         let optionButton = app.buttons.matching("label CONTAINS[c] %@", title).firstMatch
         if !optionButton.isSelected {
             optionButton.tap()
         }
         app.navigationBars.buttons["BackButton"].tap()
+        XCTAssert(app.buttons["Referral Source, \(title)"].waitForExistence(timeout: 1))
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func fillInReferralSourceOption(_ title: String) {
app.staticTexts["Referral Source"].tap()
let optionButton = app.buttons.matching("label CONTAINS[c] %@", title).firstMatch
if !optionButton.isSelected {
optionButton.tap()
}
app.navigationBars.buttons["BackButton"].tap()
}
func fillInReferralSourceOption(_ title: String) {
app.staticTexts["Referral Source"].tap()
let optionButton = app.buttons.matching("label CONTAINS[c] %@", title).firstMatch
if !optionButton.isSelected {
optionButton.tap()
}
app.navigationBars.buttons["BackButton"].tap()
XCTAssert(app.buttons["Referral Source, \(title)"].waitForExistence(timeout: 1))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MyHeartCountsUITests/DemographicsNavigator.swift` around lines 99 - 106,
`fillInReferralSourceOption(_:)` in `DemographicsNavigator` is missing the same
post-selection assertion used by the other helper flows in
`fillInDemographics()`. After tapping the referral source option and navigating
back, verify that the expected summary label/value is shown again before
returning, using the same pattern as the existing helper methods so a failed tap
is caught immediately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Development

Successfully merging this pull request may close these issues.

2 participants