Skip to content

Replace marker tutorial storyboard with SwiftUI - #297

Open
RDMurray wants to merge 1 commit into
mainfrom
agent/swiftui-marker-tutorial
Open

Replace marker tutorial storyboard with SwiftUI#297
RDMurray wants to merge 1 commit into
mainfrom
agent/swiftui-marker-tutorial

Conversation

@RDMurray

Copy link
Copy Markdown
Contributor

Summary

  • Replace MarkerTutorial.storyboard with a scrollable SwiftUI tutorial surface while retaining MarkerTutorialViewController as the UIKit coordinator.
  • Add observable tutorial presentation state and typed marker actions, including accessibility focus handling.
  • Preserve the existing audio, beacon, POI picker, marker editor, telemetry, exit, and type-based navigation behavior.
  • Route Help and POI navigation programmatically and remove stale Help/Home storyboard placeholders and segue contracts.
  • Add deterministic view-state tests plus simulator-hosted rendering and navigation-bar restoration coverage.

Why

The marker tutorial UI was coupled to a dedicated storyboard and selector-based navigation contracts. Moving the presentation to SwiftUI removes that Interface Builder dependency while keeping UIKit responsible for the tutorial lifecycle, navigation, and side effects.

User impact

The marker tutorial keeps its existing appearance and flow, with improved dynamic layout and explicit accessibility semantics. Users continue to launch it from Help, select a POI, edit a marker, hear nearby markers, configure a beacon, and exit through the existing navigation stack.

Validation

  • Built, installed, and launched Soundscape Debug on an iPhone 17 Pro simulator running iOS 26.4.
  • UnitTests: 91 passed, 0 failed.
  • Simulator-hosted SwiftUI render visually inspected.
  • InterfaceBuilderAudit: no MarkerTutorial storyboard or scene references remain.
  • Storyboard XML validation and git diff --check passed.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Rebuilt the marker tutorial with a modern SwiftUI interface.
    • Added clearer tutorial navigation, action buttons, accessibility focus handling, and exit controls.
    • Improved tutorial state updates for text, images, and available actions.
  • Bug Fixes

    • Updated tutorial navigation from Help and Home screens for more reliable presentation.
  • Tests

    • Added coverage for tutorial state behavior, navigation-bar handling, appearance, and rendered UI snapshots.

Walkthrough

Changes

The marker tutorial was migrated from storyboard/UIKit outlet rendering to a SwiftUI view backed by MarkerTutorialViewState. MarkerTutorialViewController now hosts the SwiftUI surface, Help opens it directly, obsolete storyboard routes were removed, and tests cover state, hosting, and rendering.

Marker tutorial migration

Layer / File(s) Summary
SwiftUI state and rendering
apps/ios/GuideDogs/Code/Visual UI/Views/Tutorials/MarkerTutorialView.swift
Defines tutorial actions, observable display state, accessibility behavior, conditional content, and callbacks.
Controller hosting and tutorial flow
apps/ios/GuideDogs/Code/Visual UI/View Controllers/Tutorials/Markers/MarkerTutorialViewController.swift
Hosts the SwiftUI view, supplies page data, routes text and action updates through view state, and replaces selector-based actions and storyboard exit handling.
Direct navigation and storyboard removal
apps/ios/GuideDogs/Code/Visual UI/View Controllers/Help/HelpViewController.swift, apps/ios/GuideDogs/Code/Visual UI/View Controllers/Home/HomeViewController.swift, apps/ios/GuideDogs/Code/Visual UI/Views/Help.storyboard, apps/ios/GuideDogs/Code/Visual UI/Views/main.storyboard
Help directly instantiates and pushes the marker tutorial; obsolete marker tutorial scenes, segues, and destination configuration are removed.
State tests and target registration
apps/ios/UnitTests/MarkerTutorialViewStateTests.swift, apps/ios/GuideDogs.xcodeproj/project.pbxproj
Adds coverage for tutorial state, hosted controller behavior, and simulator rendering, and registers the test file in the unit test target.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: replacing the marker tutorial storyboard with SwiftUI.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the SwiftUI tutorial migration and navigation updates.
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

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.

@RDMurray
RDMurray marked this pull request as ready for review July 28, 2026 13:16

@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

Caution

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

⚠️ Outside diff range comments (1)
apps/ios/GuideDogs/Code/Visual UI/View Controllers/Tutorials/Markers/MarkerTutorialViewController.swift (1)

285-306: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unnecessary DispatchQueue.main.async around viewState.show(...) races the accessibility notification.

show(page:) is only ever reached from main-thread contexts (viewWillAppear, or completions already dispatched via DispatchQueue.main.async/asyncAfter), so wrapping viewState.show(...) in another .main.async just defers the state write by a run-loop tick. The UIAccessibility.post(...) call right after (and the currentPageIndex/page.buttonTitle checks) run synchronously before that queued closure executes, so the layout-changed notification fires — and self.currentPageIndex is captured inside the closure — before the state update it's supposed to reflect has actually applied. The viewState.updateText(nil) call later in the same method (line 322) is invoked directly, showing the direct call is already safe here.

🐛 Proposed fix: call directly instead of deferring via async
-        DispatchQueue.main.async {
-            self.viewState.show(title: page.title,
-                                image: page.image,
-                                text: nil,
-                                actionTitle: page.buttonTitle,
-                                action: page.action,
-                                hidesContentFromAccessibility: self.currentPageIndex != UInt(PageIndexes.intro))
-        }
+        viewState.show(title: page.title,
+                       image: page.image,
+                       text: nil,
+                       actionTitle: page.buttonTitle,
+                       action: page.action,
+                       hidesContentFromAccessibility: currentPageIndex != UInt(PageIndexes.intro))
🤖 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 `@apps/ios/GuideDogs/Code/Visual` UI/View
Controllers/Tutorials/Markers/MarkerTutorialViewController.swift around lines
285 - 306, In show(page:), call viewState.show(...) directly instead of wrapping
it in DispatchQueue.main.async, so the state update completes before the
subsequent accessibility notification and page checks. Preserve the existing
show arguments and leave the later revealAction dispatch unchanged.
🧹 Nitpick comments (1)
apps/ios/GuideDogs/Code/Visual UI/View Controllers/Tutorials/Markers/MarkerTutorialViewController.swift (1)

138-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Background color literal duplicated across files.

(36/255, 58/255, 102/255) is defined as a SwiftUI Color in MarkerTutorialView.swift (lines 82-84) and re-declared here as UIColor for the container view. A shared constant (e.g. a UIColor extension consumed by both, or exposing the SwiftUI Color's components) would prevent the two from drifting apart if the palette changes.

♻️ Proposed shared constant
extension UIColor {
    static let markerTutorialBackground = UIColor(red: 36.0 / 255.0,
                                                   green: 58.0 / 255.0,
                                                   blue: 102.0 / 255.0,
                                                   alpha: 1)
}
-        view.backgroundColor = UIColor(red: 36.0 / 255.0,
-                                       green: 58.0 / 255.0,
-                                       blue: 102.0 / 255.0,
-                                       alpha: 1)
+        view.backgroundColor = .markerTutorialBackground

And in MarkerTutorialView.swift:

-    private let backgroundColor = Color(red: 36.0 / 255.0,
-                                        green: 58.0 / 255.0,
-                                        blue: 102.0 / 255.0)
+    private let backgroundColor = Color(uiColor: .markerTutorialBackground)
🤖 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 `@apps/ios/GuideDogs/Code/Visual` UI/View
Controllers/Tutorials/Markers/MarkerTutorialViewController.swift around lines
138 - 141, Replace the duplicated background color literal in
MarkerTutorialViewController with a shared marker tutorial background constant,
such as UIColor.markerTutorialBackground. Update MarkerTutorialView to consume
the same shared palette value so both UIKit and SwiftUI views remain
synchronized when the color changes.
🤖 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 `@apps/ios/UnitTests/MarkerTutorialViewStateTests.swift`:
- Around line 94-97: In apps/ios/UnitTests/MarkerTutorialViewStateTests.swift
lines 94-97, capture the original AppContext.shared.isInTutorialMode value
before the test and restore that captured value in the defer block instead of
forcing false; at lines 145-147, capture the scene’s previous key window before
makeKeyAndVisible() and restore it as key during cleanup.

---

Outside diff comments:
In `@apps/ios/GuideDogs/Code/Visual` UI/View
Controllers/Tutorials/Markers/MarkerTutorialViewController.swift:
- Around line 285-306: In show(page:), call viewState.show(...) directly instead
of wrapping it in DispatchQueue.main.async, so the state update completes before
the subsequent accessibility notification and page checks. Preserve the existing
show arguments and leave the later revealAction dispatch unchanged.

---

Nitpick comments:
In `@apps/ios/GuideDogs/Code/Visual` UI/View
Controllers/Tutorials/Markers/MarkerTutorialViewController.swift:
- Around line 138-141: Replace the duplicated background color literal in
MarkerTutorialViewController with a shared marker tutorial background constant,
such as UIColor.markerTutorialBackground. Update MarkerTutorialView to consume
the same shared palette value so both UIKit and SwiftUI views remain
synchronized when the color changes.
🪄 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: 23233c23-6821-481f-87b1-e4d2fdccc6e4

📥 Commits

Reviewing files that changed from the base of the PR and between 52bb321 and 5b8a1c4.

📒 Files selected for processing (9)
  • apps/ios/GuideDogs.xcodeproj/project.pbxproj
  • apps/ios/GuideDogs/Code/Visual UI/View Controllers/Help/HelpViewController.swift
  • apps/ios/GuideDogs/Code/Visual UI/View Controllers/Home/HomeViewController.swift
  • apps/ios/GuideDogs/Code/Visual UI/View Controllers/Tutorials/Markers/MarkerTutorialViewController.swift
  • apps/ios/GuideDogs/Code/Visual UI/Views/Help.storyboard
  • apps/ios/GuideDogs/Code/Visual UI/Views/MarkerTutorial.storyboard
  • apps/ios/GuideDogs/Code/Visual UI/Views/Tutorials/MarkerTutorialView.swift
  • apps/ios/GuideDogs/Code/Visual UI/Views/main.storyboard
  • apps/ios/UnitTests/MarkerTutorialViewStateTests.swift
💤 Files with no reviewable changes (4)
  • apps/ios/GuideDogs/Code/Visual UI/Views/MarkerTutorial.storyboard
  • apps/ios/GuideDogs/Code/Visual UI/Views/Help.storyboard
  • apps/ios/GuideDogs/Code/Visual UI/View Controllers/Home/HomeViewController.swift
  • apps/ios/GuideDogs/Code/Visual UI/Views/main.storyboard

Comment on lines +94 to +97
defer {
controller.stop()
AppContext.shared.isInTutorialMode = false
}

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

Restore the pre-test application and window state. These tests leave global state altered for later tests.

  • apps/ios/UnitTests/MarkerTutorialViewStateTests.swift#L94-L97: capture and restore the original AppContext.shared.isInTutorialMode value rather than always setting it to false.
  • apps/ios/UnitTests/MarkerTutorialViewStateTests.swift#L145-L147: capture the scene’s previous key window before makeKeyAndVisible(), then make it key again during cleanup.
📍 Affects 1 file
  • apps/ios/UnitTests/MarkerTutorialViewStateTests.swift#L94-L97 (this comment)
  • apps/ios/UnitTests/MarkerTutorialViewStateTests.swift#L145-L147
🤖 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 `@apps/ios/UnitTests/MarkerTutorialViewStateTests.swift` around lines 94 - 97,
In apps/ios/UnitTests/MarkerTutorialViewStateTests.swift lines 94-97, capture
the original AppContext.shared.isInTutorialMode value before the test and
restore that captured value in the defer block instead of forcing false; at
lines 145-147, capture the scene’s previous key window before
makeKeyAndVisible() and restore it as key during cleanup.

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.

1 participant