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") ] ) 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)