Skip to content

Isolate OTPMapProvider to the main actor - #160

Merged
aaronbrethorst merged 3 commits into
OneBusAway:mainfrom
mosliem:feature/main-actor-map-provider
Aug 22, 2026
Merged

aaronbrethorst merged 3 commits into
OneBusAway:mainfrom
mosliem:feature/main-actor-map-provider

Conversation

@mosliem

@mosliem mosliem commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Annotates OTPMapProvider with @MainActor, documenting isolation that was already
    true in practice but unstated.
  • Every call into the protocol already arrives from a main-actor context — MapCoordinator
    inside the package, host UI code outside it — and conformances drive UI: the shipped
    MKMapViewAdapter assigns an MKMapView delegate, installs a gesture recognizer and
    mutates overlays.
  • Documents the requirement where host integrators will actually meet it: the protocol's
    own doc comment and the README's "Customizing" section.

Why

Hosts that build in the Swift 6 language mode with main-actor default isolation cannot
conform to this protocol today. Their members come out @MainActor automatically, and a
main-actor-isolated method cannot satisfy a nonisolated protocol requirement, so the host
build fails outright.

The workaround available to a host — declaring the conformance nonisolated and wrapping
every body in MainActor.assumeIsolated — is boilerplate across all sixteen requirements
and asserts an invariant the package already guarantees. Stating the isolation here is
both more honest and less code.

This came out of work to render OTPKit trip plans on a SwiftUI Map in OneBusAway iOS,
which needs a second OTPMapProvider implementation backed by published state rather than
an MKMapView. That host builds in the Swift 6 language mode with the concurrency
diagnostic groups escalated to errors.

Scope

Deliberately small. MKMapViewAdapter and the test MockMapProvider infer the isolation
through their conformances and needed no changes; only the mock's test required an
explicit @MainActor.

This is a source-breaking change for any host that conforms off the main actor, which is
why it is documented rather than slipped in — we are pre-1.0, and the README now tells
implementers what the protocol expects of them. Existing callers are unaffected: every
call site in this repo, the demo app included, is already main-actor isolated.

Test plan

  • xcodebuild build -scheme OTPKit — BUILD SUCCEEDED, no new warnings
  • xcodebuild test -scheme OTPKit — 229 tests in 20 suites, TEST SUCCEEDED
  • swiftlint --strict — 0 violations in 143 files
  • Built OneBusAway iOS (OBAKit, Swift 6 language mode, main-actor default isolation,
    concurrency diagnostics as errors) against this branch via a local package path:
    BUILD SUCCEEDED, zero warnings in the five escalated concurrency groups
  • Full OBAKitTests run against this branch: 2277 tests, the only 2 failures
    reproduce identically on unmodified main

Note that this package builds with swiftLanguageModes: [.v5], so CI here cannot exercise
the strict-isolation scenario this change exists to fix — the OneBusAway build above is the
only thing that does.

Summary by CodeRabbit

  • Bug Fixes
    • Improved map provider reliability by ensuring map-related operations are performed safely on the app’s main thread.
    • Updated map integrations and validation coverage to support the improved concurrency behavior.

Every call into the protocol originates from MapCoordinator, which is
already @mainactor, and conformances drive UI — MKMapViewAdapter sets an
MKMapView delegate and mutates overlays. The isolation was real but
unstated, which left hosts building in the Swift 6 language mode with
main-actor default isolation unable to conform: their members come out
@mainactor and cannot satisfy nonisolated requirements.

MKMapViewAdapter and MockMapProvider infer the isolation through their
conformances, so only the mock's test needed an explicit @mainactor.
@coderabbitai

coderabbitai Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@aaronbrethorst, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: af306893-ea44-4f7d-89b3-87b7c6c3eff4

📥 Commits

Reviewing files that changed from the base of the PR and between b3f68b6 and ba5b00c.

📒 Files selected for processing (2)
  • OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift
  • README.markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 88fdc786-2f20-40ad-bed1-96202570defd

📥 Commits

Reviewing files that changed from the base of the PR and between a889065 and b3f68b6.

📒 Files selected for processing (2)
  • OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift
  • OTPKit/Tests/SmokeTest.swift

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The public OTPMapProvider protocol now requires main-actor isolation. The MockMapProvider test is annotated with @MainActor and includes comments that document this requirement.

Changes

Map provider isolation

Layer / File(s) Summary
Main-actor contract and test conformance
OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift, OTPKit/Tests/SmokeTest.swift
OTPMapProvider now uses @MainActor. The MockMapProvider test is also marked with @MainActor and documents the isolation requirement.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to b3f68

This change documents main-actor isolation for the map provider without altering runtime behavior, and the reported checks pass; no actionable merge-blocking risk remains.

Suggested reviewers: aaronbrethorst

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: isolating OTPMapProvider to the main actor.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

The isolation rationale claimed every call originates from MapCoordinator,
but the demo calls the provider directly (OTPDemoViewController clears routes
and annotations from its clear-trip action). The conclusion held — that view
controller is main-actor isolated too — but the sentence justifying a public
API change should not be contradicted by a file in this repo. Say what is
actually true, and add the part a host needs: conformances inherit the
isolation.

README told integrators to implement OTPMapProvider without mentioning that
conformances are now main-actor isolated, which is the one thing they have to
react to.
@mosliem
mosliem force-pushed the feature/main-actor-map-provider branch from e57755f to 12c16b7 Compare August 21, 2026 22:16
@aaronbrethorst

Copy link
Copy Markdown
Member

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

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

This is the right change, and the description does a good job of explaining why it isn't just an annotation.

I verified the premise independently rather than taking it on faith: MapCoordinator is already @MainActor and holds all 24 calls into the protocol, TripPlanner is @MainActor, MKMapViewRepresentable's makeUIView/updateUIView are main-actor through UIViewRepresentable, and the demo touches mapProvider only from viewDidLoad, an @objc action, and presentTripPlanner. So the isolation you're declaring is genuinely the one the package already relies on. Both conformances declare it on the primary declaration and pick up whole-type isolation, and MKMapViewDelegate/UIGestureRecognizerDelegate are both NS_SWIFT_UI_ACTOR in the SDK, so MKMapViewAdapter has no isolated-member-vs-nonisolated-requirement conflict.

Documenting it as source-breaking rather than slipping it in is the correct handling pre-1.0, and putting it in the protocol doc comment and the README's Customizing section is where implementers will actually hit it.

One small thing, not worth holding the merge for: the doc comment says "Conforming types inherit this isolation, so the members of a custom provider are main-actor isolated too." That's true for a conformance on the primary declaration, but a host that conforms in an extension only gets isolation on the members satisfying requirements. The README sentence steers people right either way, so take it or leave it.

Approving on the merits. Holding the actual merge only until CI is green — the failing build here is the VehicleRentalSource cancellation flake, which reproduces on unmodified main and has nothing to do with this change. #161 carries the fix for it.

@aaronbrethorst
aaronbrethorst merged commit 45438b5 into OneBusAway:main Aug 22, 2026
4 checks passed
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