From 294ceb320309896ffc2b9576aae6736ff0b42121 Mon Sep 17 00:00:00 2001 From: Mehmet Esen Date: Wed, 12 Aug 2026 16:42:20 +0300 Subject: [PATCH 1/2] chore(darwin): declare Swift 6 language mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native source already compiles clean under Swift 6 strict concurrency (previous commit fixed it) — this makes the mode official instead of approximating it with Swift 5 + -strict-concurrency=complete. - podspec: swift_version 5.0 -> 6.0, drop the now-subsumed -strict-concurrency=complete flag (kept -warnings-as-errors) - Package.swift: swift-tools-version 5.9 -> 6.0, defaults SwiftPM consumers to language mode v6 - README: document minimum requirements (iOS 17, macOS 14, Flutter 3.41/Dart 3.10, Xcode 16+) as a table Cost to users is zero: the package's iOS 17/macOS 14 floor and Flutter's own Xcode 16+ requirement mean every consumer already has a Swift 6 toolchain. Verified: example iOS simulator + macOS builds green with SWIFT_VERSION = 6.0 resolved in the pod xcconfig (Xcode 26.6). Co-Authored-By: Claude Sonnet 5 --- README.md | 9 ++++++++- darwin/mapkit_flutter.podspec | 9 ++++++--- darwin/mapkit_flutter/Package.swift | 4 +++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e21b782..f2889d2 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,14 @@ Add to `ios/Runner/Info.plist` (on macOS, `macos/Runner/Info.plist` plus the Loc Used to show your current location on the map. ``` -Minimum: iOS 17, macOS 14, Dart 3.10, Flutter 3.41. +### Minimum requirements + +| | | +|---|---| +| iOS | 17.0 | +| macOS | 14.0 | +| Flutter / Dart | 3.41 / 3.10 | +| Xcode | 16+ (the native code is Swift 6 language mode — data-race safety is compiler-enforced) | ## Quick start diff --git a/darwin/mapkit_flutter.podspec b/darwin/mapkit_flutter.podspec index e34485f..1b3bd3e 100644 --- a/darwin/mapkit_flutter.podspec +++ b/darwin/mapkit_flutter.podspec @@ -21,11 +21,14 @@ and modern MapKit configurations. s.ios.deployment_target = '17.0' s.osx.deployment_target = '14.0' - s.swift_version = '5.0' + # Swift 6 language mode: data-race safety enforced by the compiler, not a + # flag stack (subsumes the old -strict-concurrency=complete). Needs + # Xcode 16+, already below Flutter's own minimum. + s.swift_version = '6.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', - 'SWIFT_VERSION' => '5.0', - 'OTHER_SWIFT_FLAGS' => '-warnings-as-errors -strict-concurrency=complete' + 'SWIFT_VERSION' => '6.0', + 'OTHER_SWIFT_FLAGS' => '-warnings-as-errors' } end diff --git a/darwin/mapkit_flutter/Package.swift b/darwin/mapkit_flutter/Package.swift index c352c54..f971e9b 100644 --- a/darwin/mapkit_flutter/Package.swift +++ b/darwin/mapkit_flutter/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 import PackageDescription let package = Package( @@ -12,6 +12,8 @@ let package = Package( .library(name: "mapkit-flutter", targets: ["mapkit_flutter"]) ], targets: [ + // swiftLanguageMode defaults to v6 under tools 6.0 — data-race safety is + // compiler-enforced, matching the podspec's swift_version = '6.0'. .target(name: "mapkit_flutter") ] ) From 637a538ec2a763e9f2b1ce99009f69a3ca2b3054 Mon Sep 17 00:00:00 2001 From: Mehmet Esen Date: Wed, 12 Aug 2026 16:56:07 +0300 Subject: [PATCH 2/2] fix(darwin): hoist non-Sendable MapKit awaits behind sending helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's Xcode 16 SDK doesn't yet mark MKLookAroundScene and MKMapSnapshotter.Snapshot as Sendable, so under Swift 6 language mode awaiting request.scene / snapshotter.start() from @MainActor code fails there (newer SDKs annotate these and compile fine, which is why local Xcode 26.6 passed). Portable fix: each await moves into a nonisolated static helper that takes only Sendable value types (coordinate; region/size/scale/flags) and returns the result as `sending` — the request/snapshotter live entirely in a disconnected region, so region isolation proves the transfer safe on both SDK generations. Snapshot options are now built twice by design: caller-side only for size/mapRect used by the compositing pass, helper-side for the actual snapshotter. displayScale ==0 sentinel means "leave scale unset" (MapKit main-screen default), used by the macOS path. Verified: example iOS simulator + macOS builds green locally (Xcode 26.6); CI Xcode 16 is the gate this targets. Co-Authored-By: Claude Sonnet 5 --- .../MapView/MapKitViewHost.swift | 64 +++++++++++++++---- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/darwin/mapkit_flutter/Sources/mapkit_flutter/MapView/MapKitViewHost.swift b/darwin/mapkit_flutter/Sources/mapkit_flutter/MapView/MapKitViewHost.swift index 95cb7ec..539f4b6 100644 --- a/darwin/mapkit_flutter/Sources/mapkit_flutter/MapView/MapKitViewHost.swift +++ b/darwin/mapkit_flutter/Sources/mapkit_flutter/MapView/MapKitViewHost.swift @@ -164,9 +164,8 @@ public class MapKitViewHost: NSObject, @preconcurrency MapKitHostApi { #if os(iOS) let coord = coordinate.clCoordinate Task { @MainActor in - let request = MKLookAroundSceneRequest(coordinate: coord) do { - guard let scene = try await request.scene else { + guard let scene = try await Self.requestLookAroundScene(at: coord) else { completion(.success(false)) return } @@ -193,6 +192,18 @@ public class MapKitViewHost: NSObject, @preconcurrency MapKitHostApi { #endif } + #if os(iOS) + /// Runs the scene request off the main actor and hands the result back + /// with `sending`: older SDKs don't mark `MKLookAroundScene` Sendable, so + /// awaiting `request.scene` directly from `@MainActor` fails to compile + /// there. Region isolation proves the fresh scene is safe to move. + private nonisolated static func requestLookAroundScene( + at coordinate: CLLocationCoordinate2D + ) async throws -> sending MKLookAroundScene? { + try await MKLookAroundSceneRequest(coordinate: coordinate).scene + } + #endif + func addTileOverlay(overlay overlayData: PlatformTileOverlay) throws { let overlay = FlutterTileOverlay(fromPlatform: overlayData) if !overlay.id.isEmpty, tileOverlays[overlay.id] != nil { @@ -293,22 +304,25 @@ extension MapKitViewHost: MKMapViewDelegate { extension MapKitViewHost { private func takeSnapshot(options: PlatformSnapshotOptions) async throws -> FlutterStandardTypedData? { + // Caller-side options exist only for `size`/`mapRect` in the + // compositing pass below; the snapshotter gets its own copy off-actor. let snapshotOptions = MKMapSnapshotter.Options() snapshotOptions.region = self.mapView.region snapshotOptions.size = self.mapView.frame.size + #if os(iOS) - // Unspecified traits report displayScale == 0; leaving `scale` unset - // falls back to MapKit's main-screen default. + // Unspecified traits report displayScale == 0; 0 leaves `scale` unset + // so MapKit falls back to its main-screen default. let displayScale = self.mapView.traitCollection.displayScale - if displayScale > 0 { - snapshotOptions.scale = displayScale - } + #else + let displayScale: CGFloat = 0 #endif - snapshotOptions.showsBuildings = options.showsBuildings - snapshotOptions.pointOfInterestFilter = options.showsPointsOfInterest ? .includingAll : .excludingAll - - let snapshotter = MKMapSnapshotter(options: snapshotOptions) - let snapshot = try await snapshotter.start() + let snapshot = try await Self.takeSnapshot( + region: snapshotOptions.region, + size: snapshotOptions.size, + scale: displayScale, + showsBuildings: options.showsBuildings, + showsPointsOfInterest: options.showsPointsOfInterest) #if os(iOS) let image = UIGraphicsImageRenderer(size: snapshotOptions.size).image { context in @@ -340,6 +354,32 @@ extension MapKitViewHost { return FlutterStandardTypedData(bytes: imageData) } + /// Runs the snapshotter off the main actor and hands the result back with + /// `sending`: older SDKs don't mark `MKMapSnapshotter.Snapshot` Sendable, + /// so awaiting `start()` directly from `@MainActor` fails to compile + /// there. Only Sendable value types cross in; the options and snapshotter + /// live entirely in this disconnected region, so the fresh snapshot is + /// provably safe to move out. + private nonisolated static func takeSnapshot( + region: MKCoordinateRegion, + size: CGSize, + scale: CGFloat, + showsBuildings: Bool, + showsPointsOfInterest: Bool + ) async throws -> sending MKMapSnapshotter.Snapshot { + let options = MKMapSnapshotter.Options() + options.region = region + options.size = size + #if os(iOS) + if scale > 0 { + options.scale = scale + } + #endif + options.showsBuildings = showsBuildings + options.pointOfInterestFilter = showsPointsOfInterest ? .includingAll : .excludingAll + return try await MKMapSnapshotter(options: options).start() + } + #if os(iOS) private func drawAnnotations(annotation: FlutterAnnotation, point: CGPoint) { let annotationView = self.getAnnotationView(annotation: annotation)