diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 8128ccc..a1405db 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -368,7 +368,7 @@ class GoogleMapsViewImpl( var onMapError: ((RNMapErrorCode) -> Unit)? = null var onMapReady: ((Boolean) -> Unit)? = null - var onMapLoaded: ((RNRegion, RNCamera) -> Unit)? = null + var onMapLoaded: ((RNRegion, RNCameraChange) -> Unit)? = null var onLocationUpdate: ((RNLocation) -> Unit)? = null var onLocationError: ((RNLocationErrorCode) -> Unit)? = null var onMapPress: ((RNLatLng) -> Unit)? = null @@ -388,9 +388,9 @@ class GoogleMapsViewImpl( var onInfoWindowLongPress: ((String) -> Unit)? = null var onMyLocationPress: ((RNLocation) -> Unit)? = null var onMyLocationButtonPress: ((Boolean) -> Unit)? = null - var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null - var onCameraChange: ((RNRegion, RNCamera, Boolean) -> Unit)? = null - var onCameraChangeComplete: ((RNRegion, RNCamera, Boolean) -> Unit)? = null + var onCameraChangeStart: ((RNRegion, RNCameraChange, Boolean) -> Unit)? = null + var onCameraChange: ((RNRegion, RNCameraChange, Boolean) -> Unit)? = null + var onCameraChangeComplete: ((RNRegion, RNCameraChange, Boolean) -> Unit)? = null fun showMarkerInfoWindow(id: String) = onUi { diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index acb7aa0..6021830 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -305,7 +305,7 @@ class RNGoogleMapsPlusView( view.onMapReady = cb } - override var onMapLoaded: ((RNRegion, RNCamera) -> Unit)? = null + override var onMapLoaded: ((RNRegion, RNCameraChange) -> Unit)? = null set(cb) { view.onMapLoaded = cb } @@ -405,17 +405,17 @@ class RNGoogleMapsPlusView( view.onMyLocationButtonPress = cb } - override var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null + override var onCameraChangeStart: ((RNRegion, RNCameraChange, Boolean) -> Unit)? = null set(cb) { view.onCameraChangeStart = cb } - override var onCameraChange: ((RNRegion, RNCamera, Boolean) -> Unit)? = null + override var onCameraChange: ((RNRegion, RNCameraChange, Boolean) -> Unit)? = null set(cb) { view.onCameraChange = cb } - override var onCameraChangeComplete: ((RNRegion, RNCamera, Boolean) -> Unit)? = null + override var onCameraChangeComplete: ((RNRegion, RNCameraChange, Boolean) -> Unit)? = null set(cb) { view.onCameraChangeComplete = cb } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/CameraPositionExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/CameraPositionExtension.kt index 529b80d..c381e46 100644 --- a/android/src/main/java/com/rngooglemapsplus/extensions/CameraPositionExtension.kt +++ b/android/src/main/java/com/rngooglemapsplus/extensions/CameraPositionExtension.kt @@ -2,9 +2,10 @@ package com.rngooglemapsplus.extensions import com.google.android.gms.maps.model.CameraPosition import com.rngooglemapsplus.RNCamera +import com.rngooglemapsplus.RNCameraChange -fun CameraPosition.toRnCamera(): RNCamera = - RNCamera( +fun CameraPosition.toRnCamera(): RNCameraChange = + RNCameraChange( center = target.toRnLatLng(), zoom = zoom.toDouble(), bearing = bearing.toDouble(), diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index a0d55bc..91159c5 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -261,7 +261,7 @@ GMSIndoorDisplayDelegate { var onMapError: ((RNMapErrorCode) -> Void)? var onMapReady: ((Bool) -> Void)? - var onMapLoaded: ((RNRegion, RNCamera) -> Void)? + var onMapLoaded: ((RNRegion, RNCameraChange) -> Void)? var onLocationUpdate: ((RNLocation) -> Void)? var onLocationError: ((_ error: RNLocationErrorCode) -> Void)? var onMapPress: ((RNLatLng) -> Void)? @@ -281,9 +281,9 @@ GMSIndoorDisplayDelegate { var onInfoWindowLongPress: ((String) -> Void)? var onMyLocationPress: ((RNLocation) -> Void)? var onMyLocationButtonPress: ((Bool) -> Void)? - var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? - var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? - var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? + var onCameraChangeStart: ((RNRegion, RNCameraChange, Bool) -> Void)? + var onCameraChange: ((RNRegion, RNCameraChange, Bool) -> Void)? + var onCameraChangeComplete: ((RNRegion, RNCameraChange, Bool) -> Void)? @MainActor func showMarkerInfoWindow(id: String) { @@ -444,7 +444,10 @@ GMSIndoorDisplayDelegate { @MainActor func removeMarker(id: String) { - markersById.removeValue(forKey: id).map { $0.map = nil } + markersById.removeValue(forKey: id).map { + $0.icon = nil + $0.map = nil + } } @MainActor diff --git a/ios/LocationHandler.swift b/ios/LocationHandler.swift index a08501d..25ccce1 100644 --- a/ios/LocationHandler.swift +++ b/ios/LocationHandler.swift @@ -35,6 +35,8 @@ final class LocationHandler: NSObject, CLLocationManagerDelegate { func showLocationDialog() { onMainAsync { [weak self] in + guard let self = self else { return } + guard let vc = Self.topMostViewController() else { return } let title = Bundle.main.object(forInfoDictionaryKey: "LocationNotAvailableTitle") @@ -61,7 +63,7 @@ final class LocationHandler: NSObject, CLLocationManagerDelegate { title: openLocationSettingsButton ?? "Open settings", style: .default ) { _ in - self?.openLocationSettings() + self.openLocationSettings() } ) vc.present(alert, animated: true, completion: nil) diff --git a/ios/MapHelper.swift b/ios/MapHelper.swift index 162fad7..43c0573 100644 --- a/ios/MapHelper.swift +++ b/ios/MapHelper.swift @@ -32,11 +32,9 @@ func onMain(_ block: @escaping @MainActor () -> Void) { @inline(__always) func onMainAsync( - _ block: @MainActor @escaping () async -> Void + _ block: @escaping @MainActor () async -> Void ) { - if Thread.isMainThread { - Task { @MainActor in await block() } - } else { - Task { @MainActor in await block() } + Task { @MainActor in + await block() } } diff --git a/ios/MapMarkerBuilder.swift b/ios/MapMarkerBuilder.swift index 666aeb3..4432ab1 100644 --- a/ios/MapMarkerBuilder.swift +++ b/ios/MapMarkerBuilder.swift @@ -46,15 +46,35 @@ final class MapMarkerBuilder { @MainActor func update(_ prev: RNMarker, _ next: RNMarker, _ m: GMSMarker) { - if !prev.coordinateEquals(next) { - m.position = next.coordinate.toCLLocationCoordinate2D() - } + withCATransaction(disableActions: true) { + + var tracksViewChanges = false + var tracksInfoWindowChanges = false - if !prev.markerStyleEquals(next) { - buildIconAsync(next) { img in - m.tracksViewChanges = true - m.icon = img + if !prev.coordinateEquals(next) { + m.position = next.coordinate.toCLLocationCoordinate2D() + } + if !prev.markerStyleEquals(next) { + self.buildIconAsync(next) { img in + tracksViewChanges = true + m.icon = img + + if !prev.anchorEquals(next) { + m.groundAnchor = CGPoint( + x: next.anchor?.x ?? 0.5, + y: next.anchor?.y ?? 1 + ) + } + + if !prev.infoWindowAnchorEquals(next) { + m.infoWindowAnchor = CGPoint( + x: next.infoWindowAnchor?.x ?? 0.5, + y: next.infoWindowAnchor?.y ?? 0 + ) + } + } + } else { if !prev.anchorEquals(next) { m.groundAnchor = CGPoint( x: next.anchor?.x ?? 0.5, @@ -68,79 +88,70 @@ final class MapMarkerBuilder { y: next.infoWindowAnchor?.y ?? 0 ) } - - onMainAsync { [weak m] in - try? await Task.sleep(nanoseconds: 250_000_000) - m?.tracksViewChanges = false - } } - } else { - if !prev.anchorEquals(next) { - m.groundAnchor = CGPoint( - x: next.anchor?.x ?? 0.5, - y: next.anchor?.y ?? 1 - ) + + if prev.title != next.title { + tracksInfoWindowChanges = true + m.title = next.title } - if !prev.infoWindowAnchorEquals(next) { - m.infoWindowAnchor = CGPoint( - x: next.infoWindowAnchor?.x ?? 0.5, - y: next.infoWindowAnchor?.y ?? 0 - ) + if prev.snippet != next.snippet { + tracksInfoWindowChanges = true + m.snippet = next.snippet } - } - var tracksInfoWindowChanges = false + if prev.opacity != next.opacity { + let opacity = Float(next.opacity ?? 1) + m.opacity = opacity + m.iconView?.alpha = CGFloat(opacity) + } - if prev.title != next.title { - tracksInfoWindowChanges = true - m.title = next.title - } + if prev.flat != next.flat { + m.isFlat = next.flat ?? false + } - if prev.snippet != next.snippet { - tracksInfoWindowChanges = true - m.snippet = next.snippet - } + if prev.draggable != next.draggable { + m.isDraggable = next.draggable ?? false + } - if(tracksInfoWindowChanges) { - m.tracksInfoWindowChanges = true - onMainAsync { [weak m] in - try? await Task.sleep(nanoseconds: 250_000_000) - m?.tracksInfoWindowChanges = false + if prev.rotation != next.rotation { + m.rotation = next.rotation ?? 0 } - } - if prev.opacity != next.opacity { - let opacity = Float(next.opacity ?? 1) - m.opacity = opacity - m.iconView?.alpha = CGFloat(opacity) - } + if prev.zIndex != next.zIndex { + m.zIndex = Int32(next.zIndex ?? 0) + } - if prev.flat != next.flat { - m.isFlat = next.flat ?? false - } + if !prev.markerInfoWindowStyleEquals(next) { + m.tagData = MarkerTag( + id: next.id, + iconSvg: next.infoWindowIconSvg + ) + } - if prev.draggable != next.draggable { - m.isDraggable = next.draggable ?? false - } + if tracksViewChanges { + m.tracksViewChanges = tracksViewChanges + } + if tracksInfoWindowChanges { + m.tracksInfoWindowChanges = tracksInfoWindowChanges + } - if prev.rotation != next.rotation { - m.rotation = next.rotation ?? 0 - } + if tracksViewChanges || tracksInfoWindowChanges { + onMain { [weak m] in + guard let m = m else { return } - if prev.zIndex != next.zIndex { - m.zIndex = Int32(next.zIndex ?? 0) - } + if tracksViewChanges { + m.tracksViewChanges = false + } - if !prev.markerInfoWindowStyleEquals(next) { - m.tagData = MarkerTag( - id: next.id, - iconSvg: next.infoWindowIconSvg - ) + if tracksInfoWindowChanges { + m.tracksInfoWindowChanges = false + } + } + } } } - @MainActor func buildIconAsync( _ m: RNMarker, onReady: @escaping (UIImage?) -> Void @@ -158,12 +169,15 @@ final class MapMarkerBuilder { return } + let scale = UIScreen.main.scale + let task = Task(priority: .userInitiated) { [weak self] in guard let self else { return } - defer { self.tasks.removeValue(forKey: m.id) } + defer { + Task { @MainActor in self.tasks.removeValue(forKey: m.id) } + } - let scale = UIScreen.main.scale - let img = await self.renderUIImage(m, scale) + let img = self.renderUIImage(m, scale) guard let img, !Task.isCancelled else { return } self.iconCache.setObject(img, forKey: key) @@ -212,18 +226,11 @@ final class MapMarkerBuilder { svgImg.size = size - guard let base = svgImg.uiImage else { + guard let finalImage = SVGKExporterUIImage.export(asUIImage: svgImg) else { + svgImg.clear() return nil } - - let fmt = UIGraphicsImageRendererFormat.default() - fmt.opaque = false - fmt.scale = UIScreen.main.scale - let renderer = UIGraphicsImageRenderer(size: size, format: fmt) - - let finalImage = renderer.image { _ in - base.draw(in: CGRect(origin: .zero, size: size)) - } + svgImg.clear() let imageView = UIImageView(image: finalImage) imageView.frame = CGRect(origin: .zero, size: size) @@ -233,10 +240,10 @@ final class MapMarkerBuilder { return imageView } - @MainActor - private func renderUIImage(_ m: RNMarker, _ scale: CGFloat) async -> UIImage? { - guard let iconSvg = m.iconSvg, - let data = iconSvg.svgString.data(using: .utf8) + private func renderUIImage(_ m: RNMarker, _ scale: CGFloat) -> UIImage? { + guard + let iconSvg = m.iconSvg, + let data = iconSvg.svgString.data(using: .utf8) else { return nil } let size = CGSize( @@ -244,28 +251,21 @@ final class MapMarkerBuilder { height: max(1, CGFloat(iconSvg.height)) ) - return await Task.detached(priority: .userInitiated) { - autoreleasepool { - guard let svgImg = SVGKImage(data: data) else { return nil } - svgImg.size = size + return autoreleasepool { () -> UIImage? in + guard !Task.isCancelled else { return nil } + guard let svgImg = SVGKImage(data: data) else { return nil } - guard !Task.isCancelled else { return nil } - guard let base = svgImg.uiImage else { return nil } + svgImg.size = size - if let cg = base.cgImage { - return UIImage(cgImage: cg, scale: scale, orientation: .up) - } - guard !Task.isCancelled else { return nil } - let fmt = UIGraphicsImageRendererFormat.default() - fmt.opaque = false - fmt.scale = scale - guard !Task.isCancelled else { return nil } - let renderer = UIGraphicsImageRenderer(size: size, format: fmt) - return renderer.image { _ in - base.draw(in: CGRect(origin: .zero, size: size)) - } + guard !Task.isCancelled else { + svgImg.clear() + return nil } - }.value + + let uiImage = SVGKExporterUIImage.export(asUIImage: svgImg) + svgImg.clear() + return uiImage + } } } diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index cef7c5d..bbd9437 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -131,26 +131,26 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { ) let removed = Set(prevById.keys).subtracting(nextById.keys) - withCATransaction(disableActions: true) { - removed.forEach { - self.impl.removeMarker(id: $0) - self.markerBuilder.cancelIconTask($0) - } + removed.forEach { + self.impl.removeMarker(id: $0) + self.markerBuilder.cancelIconTask($0) + } - for (id, next) in nextById { - if let prev = prevById[id] { - if !prev.markerEquals(next) { - self.impl.updateMarker(id: id) { m in - self.markerBuilder.update(prev, next, m) - } - } - } else { - self.markerBuilder.buildIconAsync(next) { icon in - let marker = self.markerBuilder.build(next, icon: icon) - self.impl.addMarker(id: id, marker: marker) + for (id, next) in nextById { + if let prev = prevById[id] { + if !prev.markerEquals(next) { + self.impl.updateMarker(id: id) { [weak self] m in + guard let self else { return } + self.markerBuilder.update(prev, next, m) } } + } else { + self.markerBuilder.buildIconAsync(next) { [weak self] icon in + guard let self else { return } + let marker = self.markerBuilder.build(next, icon: icon) + self.impl.addMarker(id: id, marker: marker) + } } } } @@ -174,7 +174,8 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { for (id, next) in nextById { if let prev = prevById[id] { if !prev.polylineEquals(next) { - impl.updatePolyline(id: id) { pl in + impl.updatePolyline(id: id) { [weak self] pl in + guard let self else { return } self.polylineBuilder.update(prev, next, pl) } } @@ -206,7 +207,8 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { for (id, next) in nextById { if let prev = prevById[id] { if !prev.polygonEquals(next) { - impl.updatePolygon(id: id) { pg in + impl.updatePolygon(id: id) { [weak self] pg in + guard let self else { return } self.polygonBuilder.update(prev, next, pg) } } @@ -235,7 +237,8 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { for (id, next) in nextById { if let prev = prevById[id] { if !prev.circleEquals(next) { - impl.updateCircle(id: id) { circle in + impl.updateCircle(id: id) { [weak self] circle in + guard let self else { return } self.circleBuilder.update(prev, next, circle) } } @@ -328,7 +331,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { didSet { impl.onMapReady = onMapReady } } @MainActor - var onMapLoaded: ((RNRegion, RNCamera) -> Void)? { + var onMapLoaded: ((RNRegion, RNCameraChange) -> Void)? { didSet { impl.onMapLoaded = onMapLoaded } } @MainActor @@ -408,26 +411,26 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { didSet { impl.onMyLocationButtonPress = onMyLocationButtonPress } } @MainActor - var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? { + var onCameraChangeStart: ((RNRegion, RNCameraChange, Bool) -> Void)? { didSet { impl.onCameraChangeStart = onCameraChangeStart } } @MainActor - var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? { + var onCameraChange: ((RNRegion, RNCameraChange, Bool) -> Void)? { didSet { impl.onCameraChange = onCameraChange } } @MainActor - var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? { + var onCameraChangeComplete: ((RNRegion, RNCameraChange, Bool) -> Void)? { didSet { impl.onCameraChangeComplete = onCameraChangeComplete } } @MainActor func showMarkerInfoWindow(id: String) { - impl.showMarkerInfoWindow(id: id); + impl.showMarkerInfoWindow(id: id) } @MainActor func hideMarkerInfoWindow(id: String) { - impl.hideMarkerInfoWindow(id: id); + impl.hideMarkerInfoWindow(id: id) } @MainActor diff --git a/ios/extensions/GMSCameraPosition+Extension.swift b/ios/extensions/GMSCameraPosition+Extension.swift index 016e5af..48177c5 100644 --- a/ios/extensions/GMSCameraPosition+Extension.swift +++ b/ios/extensions/GMSCameraPosition+Extension.swift @@ -1,8 +1,8 @@ import GoogleMaps extension GMSCameraPosition { - func toRNCamera() -> RNCamera { - return RNCamera( + func toRNCamera() -> RNCameraChange { + return RNCameraChange( center: target.toRNLatLng(), zoom: Double(zoom), bearing: bearing, diff --git a/ios/extensions/SVGKImage+Extension.swift b/ios/extensions/SVGKImage+Extension.swift new file mode 100644 index 0000000..71a0371 --- /dev/null +++ b/ios/extensions/SVGKImage+Extension.swift @@ -0,0 +1,22 @@ +import SVGKit + +extension SVGKImage { + @inline(__always) + func clear() { + caLayerTree?.sublayers?.forEach { $0.removeFromSuperlayer() } + if let layer = caLayerTree { + layer.sublayers?.forEach { $0.removeFromSuperlayer() } + layer.removeFromSuperlayer() + } + + if let dom = domDocument { + dom.childNodes = nil + } + + if let root = domTree { + root.childNodes = nil + } + + caLayerTree.sublayers?.removeAll() + } +} diff --git a/src/RNGoogleMapsPlusView.nitro.ts b/src/RNGoogleMapsPlusView.nitro.ts index 06f9b0f..ce664b7 100644 --- a/src/RNGoogleMapsPlusView.nitro.ts +++ b/src/RNGoogleMapsPlusView.nitro.ts @@ -5,6 +5,7 @@ import type { } from 'react-native-nitro-modules'; import type { RNCamera, + RNCameraChange, RNLatLng, RNMapPadding, RNPolygon, @@ -53,7 +54,7 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { locationConfig?: RNLocationConfig; onMapError?: (error: RNMapErrorCode) => void; onMapReady?: (ready: boolean) => void; - onMapLoaded?: (region: RNRegion, camera: RNCamera) => void; + onMapLoaded?: (region: RNRegion, camera: RNCameraChange) => void; onLocationUpdate?: (location: RNLocation) => void; onLocationError?: (error: RNLocationErrorCode) => void; onMapPress?: (coordinate: RNLatLng) => void; @@ -75,17 +76,17 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { onMyLocationButtonPress?: (pressed: boolean) => void; onCameraChangeStart?: ( region: RNRegion, - camera: RNCamera, + camera: RNCameraChange, isGesture: boolean ) => void; onCameraChange?: ( region: RNRegion, - camera: RNCamera, + camera: RNCameraChange, isGesture: boolean ) => void; onCameraChangeComplete?: ( region: RNRegion, - camera: RNCamera, + camera: RNCameraChange, isGesture: boolean ) => void; } diff --git a/src/types.ts b/src/types.ts index b1a51a1..bda4057 100644 --- a/src/types.ts +++ b/src/types.ts @@ -130,6 +130,13 @@ export interface RNMapStyleElement { stylers: RNMapStyler[]; } +export type RNCameraChange = { + center: RNLatLng; + zoom: number; + bearing: number; + tilt: number; +}; + export type RNCamera = { center?: RNLatLng; zoom?: number;