Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class AnnotationIcon: Equatable {
case .image:
self.iconType = .image
if let data = icon.imageBytes?.data {
self.image = PlatformImage.fromImageData(data)
self.image = PlatformImage.fromImageData(
data, pixelRatio: CGFloat(icon.imagePixelRatio ?? 1))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public final class MapKitPlugin: NSObject, FlutterPlugin {
// Plugin registration runs on the main thread; the factory and view
// host are main-actor isolated. macOS's GeneratedPluginRegistrant
// calls this from a nonisolated context, so assume isolation here.
// The registrar hand-off shares that guarantee: it never leaves the
// main thread, it just isn't Sendable-provable.
nonisolated(unsafe) let registrar = registrar
MainActor.assumeIsolated {
let factory = MapKitViewFactory(withRegistrar: registrar)
#if os(iOS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import AppKit
import FlutterMacOS
#endif

class FlutterMapView: MKMapView, PlatformGestureRecognizerDelegate, CLLocationManagerDelegate {
// `@preconcurrency`: CLLocationManagerDelegate is nonisolated, but callbacks
// arrive on the thread that created the manager — main, since the manager is
// an instance property of this MainActor-isolated view.
class FlutterMapView: MKMapView, PlatformGestureRecognizerDelegate, @preconcurrency CLLocationManagerDelegate {
weak var flutterApi: MapKitFlutterApi?

/// Camera handed over before the first layout pass; re-applied once the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,12 @@ extension MapKitViewHost {
snapshotOptions.region = self.mapView.region
snapshotOptions.size = self.mapView.frame.size
#if os(iOS)
snapshotOptions.scale = PlatformScreen.scale
// Unspecified traits report displayScale == 0; leaving `scale` unset
// falls back to MapKit's main-screen default.
let displayScale = self.mapView.traitCollection.displayScale
if displayScale > 0 {
snapshotOptions.scale = displayScale
}
#endif
snapshotOptions.showsBuildings = options.showsBuildings
snapshotOptions.pointOfInterestFilter = options.showsPointsOfInterest ? .includingAll : .excludingAll
Expand Down Expand Up @@ -367,7 +372,10 @@ extension MapKitViewHost {
}

#if os(iOS)
extension MapKitViewHost: FlutterPlatformView {
// `@preconcurrency`: FlutterPlatformView is nonisolated; the engine calls
// `view()` on the platform thread (main), same as the MapKitHostApi
// conformance above.
extension MapKitViewHost: @preconcurrency FlutterPlatformView {
public func view() -> UIView {
return contentView
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ final class FlutterGeodesicPolyline: MKGeodesicPolyline, StyledPolyline, @unchec

public extension MKPolyline {
// maxMeters is the preferred distance offset from the self to be acknowledged as a touch
@MainActor
func contains(coordinate: CLLocationCoordinate2D, mapView: MKMapView, maxMeters: Int = 8) -> Bool {
let distance: Double = distanceOf(pt: MKMapPoint.init(coordinate), toMultipPoint: self)
return distance <= meters(fromPixel: maxMeters, at: coordinate, view: mapView)
Expand Down Expand Up @@ -200,6 +201,7 @@ public extension MKPolyline {
return distance
}

@MainActor
private func meters(fromPixel pixel: Int, at touchCoordinate: CLLocationCoordinate2D, view: MKMapView) -> Double {
let touchPoint: CGPoint = view.convert(touchCoordinate, toPointTo: view)
let maxOffsetPoint = CGPoint(x: touchPoint.x + CGFloat(pixel), y: touchPoint.y)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,6 @@ extension PlatformView {
}
}

enum PlatformScreen {
/// Backing scale factor of the main screen (`UIScreen.scale` /
/// `NSScreen.backingScaleFactor`). Defaults to 2 when unavailable.
static var scale: CGFloat {
#if os(iOS)
return UIScreen.main.scale
#elseif os(macOS)
return NSScreen.main?.backingScaleFactor ?? 2
#endif
}
}

extension PlatformImage {
/// An SF Symbol image by name (`UIImage(systemName:)` /
/// `NSImage(systemSymbolName:)`).
Expand All @@ -65,19 +53,18 @@ extension PlatformImage {
#endif
}

/// A bitmap image from raw PNG/image bytes, sized in points to match the
/// screen scale (`UIImage(data:scale:)` has no AppKit equivalent, so the
/// macOS path divides the pixel size by the backing scale).
static func fromImageData(_ data: Data) -> PlatformImage? {
/// A bitmap image from raw PNG/image bytes, sized in points via the
/// Dart-supplied pixel ratio of the bytes (`UIImage(data:scale:)` has no
/// AppKit equivalent, so the macOS path divides the pixel size by it).
static func fromImageData(_ data: Data, pixelRatio: CGFloat) -> PlatformImage? {
#if os(iOS)
return UIImage(data: data, scale: PlatformScreen.scale)
return UIImage(data: data, scale: pixelRatio)
#elseif os(macOS)
guard let image = NSImage(data: data) else { return nil }
if let rep = image.representations.first {
let scale = PlatformScreen.scale
image.size = NSSize(
width: CGFloat(rep.pixelsWide) / scale,
height: CGFloat(rep.pixelsHigh) / scale
width: CGFloat(rep.pixelsWide) / pixelRatio,
height: CGFloat(rep.pixelsHigh) / pixelRatio
)
}
return image
Expand Down
Loading
Loading