From b3f68b62eafd9e5be0daf80b7dcbc56992408e3d Mon Sep 17 00:00:00 2001 From: mosliem Date: Fri, 21 Aug 2026 23:20:07 +0300 Subject: [PATCH 1/2] Isolate OTPMapProvider to the main actor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift | 7 +++++++ OTPKit/Tests/SmokeTest.swift | 3 +++ 2 files changed, 10 insertions(+) diff --git a/OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift b/OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift index f1269e1..361c2c6 100644 --- a/OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift +++ b/OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift @@ -13,6 +13,13 @@ import SwiftUI /// Protocol defining the interface for map operations that OTPKit requires. /// Implementers of this protocol can provide their own map view (MKMapView, custom map, etc.) /// while allowing OTPKit to control map content and interactions. +/// +/// Main-actor isolated because every call originates from `MapCoordinator`, which is +/// itself `@MainActor`, and conformances drive UI. Stating that here rather than leaving +/// it implicit lets hosts building in the Swift 6 language mode with main-actor default +/// isolation conform directly, instead of opting the conformance out of isolation and +/// hopping back in every method body. +@MainActor public protocol OTPMapProvider: AnyObject { // MARK: - Route Display diff --git a/OTPKit/Tests/SmokeTest.swift b/OTPKit/Tests/SmokeTest.swift index 552e5e0..02f02d2 100644 --- a/OTPKit/Tests/SmokeTest.swift +++ b/OTPKit/Tests/SmokeTest.swift @@ -27,6 +27,9 @@ func testFixturesCreatePlace() { #expect(place.lon == -122.0) } +// `MockMapProvider` conforms to `OTPMapProvider`, which is `@MainActor`, so the +// mock picks up that isolation and the test has to run there too. +@MainActor @Test("MockMapProvider - tracks addRoute calls") func mockMapProviderTracksRouteCalls() { let mockMap = MockMapProvider() From 12c16b7321cc212b1c80aa344a37a5cf3e9aaadf Mon Sep 17 00:00:00 2001 From: mosliem Date: Sat, 22 Aug 2026 00:59:30 +0300 Subject: [PATCH 2/2] Document the main-actor requirement for host implementers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift | 13 ++++++++----- README.markdown | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift b/OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift index 361c2c6..e5b5911 100644 --- a/OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift +++ b/OTPKit/Sources/OTPKit/Core/Map/OTPMapProvider.swift @@ -14,11 +14,14 @@ import SwiftUI /// Implementers of this protocol can provide their own map view (MKMapView, custom map, etc.) /// while allowing OTPKit to control map content and interactions. /// -/// Main-actor isolated because every call originates from `MapCoordinator`, which is -/// itself `@MainActor`, and conformances drive UI. Stating that here rather than leaving -/// it implicit lets hosts building in the Swift 6 language mode with main-actor default -/// isolation conform directly, instead of opting the conformance out of isolation and -/// hopping back in every method body. +/// Main-actor isolated because every call arrives from a main-actor context — from +/// `MapCoordinator` inside the package, and from host UI code outside it — and because +/// conformances drive UI. Conforming types inherit this isolation, so the members of a +/// custom provider are main-actor isolated too. +/// +/// Stating that here rather than leaving it implicit lets hosts building in the Swift 6 +/// language mode with main-actor default isolation conform directly, instead of opting +/// the conformance out of isolation and hopping back in every method body. @MainActor public protocol OTPMapProvider: AnyObject { diff --git a/README.markdown b/README.markdown index 6026e3d..7f1e685 100644 --- a/README.markdown +++ b/README.markdown @@ -112,7 +112,7 @@ Both are Swift actors conforming to `APIService`; you can also implement `APISer - **Transport modes:** pass `enabledTransportModes` to `OTPConfiguration` (defaults to transit, walk, bike, car). Rental modes like `.bikeRental` are opt-in and need an OTP 2.x server with rental data. - **Theme:** pass an `OTPThemeConfiguration` to adjust colors. -- **Map behavior:** implement `OTPMapProvider` to control exactly how routes and stops render on your map. +- **Map behavior:** implement `OTPMapProvider` to control exactly how routes and stops render on your map. The protocol is `@MainActor`, so conformances inherit main-actor isolation — write your provider as main-actor isolated rather than opting the conformance out. ### Localization