From f15d6388911943b5abdfd9d5f61e3423af33f064 Mon Sep 17 00:00:00 2001 From: pinpong Date: Wed, 8 Oct 2025 16:29:28 +0700 Subject: [PATCH] refactor(map): unify update logic and defaults across Android and iOS --- .github/workflows/release.yml | 2 +- .../pull_request_template.md | 25 +- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 139 +++----- .../com/rngooglemapsplus/LocationHandler.kt | 6 +- .../com/rngooglemapsplus/MapCircleBuilder.kt | 18 +- .../com/rngooglemapsplus/MapMarkerBuilder.kt | 25 ++ .../com/rngooglemapsplus/MapPolygonBuilder.kt | 19 +- .../rngooglemapsplus/MapPolylineBuilder.kt.kt | 35 +- .../rngooglemapsplus/RNGoogleMapsPlusView.kt | 118 +++---- example/ios/Podfile.lock | 4 +- example/src/App.tsx | 152 ++++----- ios/GoogleMapViewImpl.swift | 306 ++++++------------ ios/LocationHandler.swift | 12 +- ios/MapCircleBuilder.swift | 31 +- ios/MapHelper.swift | 10 +- ios/MapMarkerBuilder.swift | 80 +++-- ios/MapPolygonBuilder.swift | 32 +- ios/MapPolylineBuilder.swift | 38 ++- ios/RNGoogleMapsPlusView.swift | 23 +- ios/extensions/RNCircle+Extension.swift | 13 - .../RNPolygon+Extension.swift.swift | 21 +- .../RNPolyline+Extension.swift.swift | 27 +- src/RNGoogleMapsPlusView.nitro.ts | 4 +- src/types.ts | 7 +- 24 files changed, 505 insertions(+), 642 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 038988a..8b50e6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: concurrency: group: release-${{ github.ref_name }} cancel-in-progress: true - + permissions: contents: read diff --git a/PULL_REQUEST_TEMPLATE/pull_request_template.md b/PULL_REQUEST_TEMPLATE/pull_request_template.md index dbd3c38..cc491ce 100644 --- a/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -14,29 +14,33 @@ CI already runs linting, formatting, and build checks automatically. --- ### Summary + Short description of what this PR changes or adds. --- ### Type of change -- [ ] Feature -- [ ] Fix -- [ ] Refactor -- [ ] Internal / CI -- [ ] Documentation + +- [ ] Feature +- [ ] Fix +- [ ] Refactor +- [ ] Internal / CI +- [ ] Documentation --- ### Scope -- [ ] Android -- [ ] iOS -- [ ] Core -- [ ] Example App -- [ ] Docs + +- [ ] Android +- [ ] iOS +- [ ] Core +- [ ] Example App +- [ ] Docs --- ### Related + List any related issues, pull requests, or discussions. Use the format: `Fixes #123`, `Refs #456`, `Close #789` @@ -44,4 +48,5 @@ Use the format: --- ### Additional notes + _(Optional – anything relevant for the reviewer)_ diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 0f4f13c..d24d0e9 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -293,11 +293,9 @@ class GoogleMapsViewImpl( userInterfaceStyle?.let { googleMap?.mapColorScheme = it } - minZoomLevel?.let { - googleMap?.setMinZoomPreference(it.toFloat()) - } - maxZoomLevel?.let { - googleMap?.setMaxZoomPreference(it.toFloat()) + mapZoomConfig?.let { + googleMap?.setMinZoomPreference(it.min?.toFloat() ?: 2.0f) + googleMap?.setMaxZoomPreference(it.max?.toFloat() ?: 21.0f) } } @@ -340,56 +338,34 @@ class GoogleMapsViewImpl( set(value) { field = value onUi { - value?.let { v -> - googleMap?.uiSettings?.apply { - v.allGesturesEnabled?.let { setAllGesturesEnabled(it) } - v.compassEnabled?.let { isCompassEnabled = it } - v.indoorLevelPickerEnabled?.let { isIndoorLevelPickerEnabled = it } - v.mapToolbarEnabled?.let { isMapToolbarEnabled = it } - v.myLocationButtonEnabled?.let { - googleMap?.setLocationSource(locationHandler) - isMyLocationButtonEnabled = it - } - v.rotateEnabled?.let { isRotateGesturesEnabled = it } - v.scrollEnabled?.let { isScrollGesturesEnabled = it } - v.scrollDuringRotateOrZoomEnabled?.let { - isScrollGesturesEnabledDuringRotateOrZoom = it - } - v.tiltEnabled?.let { isTiltGesturesEnabled = it } - v.zoomControlsEnabled?.let { isZoomControlsEnabled = it } - v.zoomGesturesEnabled?.let { isZoomGesturesEnabled = it } - } + googleMap?.uiSettings?.apply { + setAllGesturesEnabled(value?.allGesturesEnabled ?: true) + isCompassEnabled = value?.compassEnabled ?: false + isIndoorLevelPickerEnabled = value?.indoorLevelPickerEnabled ?: false + isMapToolbarEnabled = value?.mapToolbarEnabled ?: false + + val myLocationEnabled = value?.myLocationButtonEnabled ?: false + googleMap?.setLocationSource(if (myLocationEnabled) locationHandler else null) + isMyLocationButtonEnabled = myLocationEnabled + + isRotateGesturesEnabled = value?.rotateEnabled ?: true + isScrollGesturesEnabled = value?.scrollEnabled ?: true + isScrollGesturesEnabledDuringRotateOrZoom = + value?.scrollDuringRotateOrZoomEnabled ?: true + isTiltGesturesEnabled = value?.tiltEnabled ?: true + isZoomControlsEnabled = value?.zoomControlsEnabled ?: false + isZoomGesturesEnabled = value?.zoomGesturesEnabled ?: false } - ?: run { - googleMap?.uiSettings?.apply { - setAllGesturesEnabled(true) - isCompassEnabled = false - isIndoorLevelPickerEnabled = false - isMapToolbarEnabled = false - isMyLocationButtonEnabled = false - googleMap?.setLocationSource(null) - isRotateGesturesEnabled = true - isScrollGesturesEnabled = true - isScrollGesturesEnabledDuringRotateOrZoom = true - isTiltGesturesEnabled = true - isZoomControlsEnabled = false - isZoomGesturesEnabled = false - } - } } } @SuppressLint("MissingPermission") var myLocationEnabled: Boolean? = null set(value) { + field = value onUi { try { - value?.let { - googleMap?.isMyLocationEnabled = it - } - ?: run { - googleMap?.isMyLocationEnabled = false - } + googleMap?.isMyLocationEnabled = value ?: false } catch (se: SecurityException) { onLocationError?.invoke(RNLocationErrorCode.PERMISSION_DENIED) } catch (ex: Exception) { @@ -403,12 +379,7 @@ class GoogleMapsViewImpl( set(value) { field = value onUi { - value?.let { - googleMap?.isBuildingsEnabled = it - } - ?: run { - googleMap?.isBuildingsEnabled = false - } + googleMap?.isBuildingsEnabled = value ?: false } } @@ -416,11 +387,7 @@ class GoogleMapsViewImpl( set(value) { field = value onUi { - value?.let { - googleMap?.isTrafficEnabled = it - } ?: run { - googleMap?.isTrafficEnabled = false - } + googleMap?.isTrafficEnabled = value ?: false } } @@ -428,12 +395,7 @@ class GoogleMapsViewImpl( set(value) { field = value onUi { - value?.let { - googleMap?.isIndoorEnabled = it - } - ?: run { - googleMap?.isIndoorEnabled = false - } + googleMap?.isIndoorEnabled = value ?: false } } @@ -449,52 +411,29 @@ class GoogleMapsViewImpl( set(value) { field = value onUi { - value?.let { - googleMap?.mapColorScheme = it - } ?: run { - googleMap?.mapColorScheme = MapColorScheme.FOLLOW_SYSTEM - } + googleMap?.mapColorScheme = value ?: MapColorScheme.FOLLOW_SYSTEM } } - var minZoomLevel: Double? = null + var mapZoomConfig: RNMapZoomConfig? = null set(value) { field = value onUi { - value?.let { - googleMap?.setMinZoomPreference(it.toFloat()) - } ?: run { - googleMap?.setMinZoomPreference(2.0f) - } - } - } - - var maxZoomLevel: Double? = null - set(value) { - field = value - onUi { - value?.let { - googleMap?.setMaxZoomPreference(it.toFloat()) - } ?: run { - googleMap?.setMaxZoomPreference(21.0f) - } + googleMap?.setMinZoomPreference(value?.min?.toFloat() ?: 2.0f) + googleMap?.setMaxZoomPreference(value?.max?.toFloat() ?: 21.0f) } } var mapPadding: RNMapPadding? = null set(value) { field = value - value?.let { - onUi { - googleMap?.setPadding( - it.left.dpToPx().toInt(), - it.top.dpToPx().toInt(), - it.right.dpToPx().toInt(), - it.bottom.dpToPx().toInt(), - ) - } - } ?: run { - googleMap?.setPadding(0, 0, 0, 0) + onUi { + googleMap?.setPadding( + value?.left?.dpToPx()?.toInt() ?: 0, + value?.top?.dpToPx()?.toInt() ?: 0, + value?.right?.dpToPx()?.toInt() ?: 0, + value?.bottom?.dpToPx()?.toInt() ?: 0, + ) } } @@ -502,11 +441,7 @@ class GoogleMapsViewImpl( set(value) { field = value onUi { - value?.let { - googleMap?.mapType = it - } ?: run { - googleMap?.mapType = 1 - } + googleMap?.mapType = value ?: 1 } } diff --git a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt index 32d61ba..42313db 100644 --- a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt +++ b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt @@ -38,19 +38,19 @@ class LocationHandler( var priority: Int? = PRIORITY_DEFAULT set(value) { - field = value + field = value ?: PRIORITY_DEFAULT start() } var interval: Long? = INTERVAL_DEFAULT set(value) { - field = value + field = value ?: INTERVAL_DEFAULT buildLocationRequest() } var minUpdateInterval: Long? = MIN_UPDATE_INTERVAL set(value) { - field = value + field = value ?: MIN_UPDATE_INTERVAL buildLocationRequest() } diff --git a/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt index f4ac789..07a98f5 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt @@ -1,19 +1,33 @@ package com.rngooglemapsplus +import android.graphics.Color import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.CircleOptions import com.google.android.gms.maps.model.LatLng import com.rngooglemapsplus.extensions.toColor class MapCircleBuilder { - fun buildCircleOptions(circle: RNCircle): CircleOptions = + fun build(circle: RNCircle): CircleOptions = CircleOptions().apply { center(LatLng(circle.center.latitude, circle.center.longitude)) - circle.radius?.let { radius(it) } + radius(circle.radius) circle.strokeWidth?.let { strokeWidth(it.dpToPx()) } circle.strokeColor?.let { strokeColor(it.toColor()) } circle.fillColor?.let { fillColor(it.toColor()) } circle.pressable?.let { clickable(it) } circle.zIndex?.let { zIndex(it.toFloat()) } } + + fun update( + circle: Circle, + next: RNCircle, + ) { + circle.center = LatLng(next.center.latitude, next.center.longitude) + circle.radius = next.radius + circle.strokeWidth = next.strokeWidth?.dpToPx() ?: 1f + circle.strokeColor = next.strokeColor?.toColor() ?: Color.BLACK + circle.fillColor = next.fillColor?.toColor() ?: Color.TRANSPARENT + circle.zIndex = next.zIndex?.toFloat() ?: 0f + } } diff --git a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt index dcf70f6..abd0d86 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt @@ -9,7 +9,9 @@ import com.facebook.react.uimanager.PixelUtil.dpToPx import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions +import com.rngooglemapsplus.extensions.markerStyleEquals import com.rngooglemapsplus.extensions.styleHash import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -44,6 +46,29 @@ class MapMarkerBuilder( m.zIndex?.let { zIndex(it.toFloat()) } } + fun update( + marker: Marker, + prev: RNMarker, + next: RNMarker, + ) { + marker.position = + LatLng( + next.coordinate.latitude, + next.coordinate.longitude, + ) + marker.zIndex = next.zIndex?.toFloat() ?: 0f + + if (!prev.markerStyleEquals(next)) { + buildIconAsync(marker.id, next) { icon -> + marker.setIcon(icon) + } + } + marker.setAnchor( + (next.anchor?.x ?: 0.5).toFloat(), + (next.anchor?.y ?: 0.5).toFloat(), + ) + } + fun buildIconAsync( id: String, m: RNMarker, diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt index 4bae53b..f3173ae 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt @@ -1,11 +1,14 @@ package com.rngooglemapsplus +import android.graphics.Color import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.Polygon import com.google.android.gms.maps.model.PolygonOptions import com.rngooglemapsplus.extensions.toColor class MapPolygonBuilder { - fun buildPolygonOptions(poly: RNPolygon): PolygonOptions = + fun build(poly: RNPolygon): PolygonOptions = PolygonOptions().apply { poly.coordinates.forEach { pt -> add( @@ -19,4 +22,18 @@ class MapPolygonBuilder { poly.pressable?.let { clickable(it) } poly.zIndex?.let { zIndex(it.toFloat()) } } + + fun update( + gmsPoly: Polygon, + next: RNPolygon, + ) { + gmsPoly.points = + next.coordinates.map { + LatLng(it.latitude, it.longitude) + } + gmsPoly.fillColor = next.fillColor?.toColor() ?: Color.TRANSPARENT + gmsPoly.strokeColor = next.strokeColor?.toColor() ?: Color.BLACK + gmsPoly.strokeWidth = next.strokeWidth?.dpToPx() ?: 1f + gmsPoly.zIndex = next.zIndex?.toFloat() ?: 0f + } } diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt b/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt index 411c35f..f5571a9 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt @@ -1,40 +1,57 @@ package com.rngooglemapsplus +import android.graphics.Color import com.facebook.react.uimanager.PixelUtil.dpToPx import com.google.android.gms.maps.model.ButtCap import com.google.android.gms.maps.model.Cap import com.google.android.gms.maps.model.JointType +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.Polyline import com.google.android.gms.maps.model.PolylineOptions import com.google.android.gms.maps.model.RoundCap import com.google.android.gms.maps.model.SquareCap import com.rngooglemapsplus.extensions.toColor class MapPolylineBuilder { - fun buildPolylineOptions(pl: RNPolyline): PolylineOptions = + fun build(pl: RNPolyline): PolylineOptions = PolylineOptions().apply { pl.coordinates.forEach { pt -> - add( - com.google.android.gms.maps.model - .LatLng(pt.latitude, pt.longitude), - ) + add(LatLng(pt.latitude, pt.longitude)) } pl.width?.let { width(it.dpToPx()) } - pl.lineCap?.let { startCap(mapLineCap(it)) } - pl.lineCap?.let { endCap(mapLineCap(it)) } + pl.lineCap?.let { + startCap(mapLineCap(it)) + endCap(mapLineCap(it)) + } pl.lineJoin?.let { jointType(mapLineJoin(it)) } pl.color?.let { color(it.toColor()) } pl.pressable?.let { clickable(it) } pl.zIndex?.let { zIndex(it.toFloat()) } } - fun mapLineCap(type: RNLineCapType?): Cap = + fun update( + polyline: Polyline, + next: RNPolyline, + ) { + polyline.points = next.coordinates.map { LatLng(it.latitude, it.longitude) } + + polyline.width = next.width?.dpToPx() ?: 1f + val cap = mapLineCap(next.lineCap ?: RNLineCapType.BUTT) + polyline.startCap = cap + polyline.endCap = cap + polyline.jointType = mapLineJoin(next.lineJoin ?: RNLineJoinType.MITER) + polyline.color = next.color?.toColor() ?: Color.BLACK + polyline.zIndex = next.zIndex?.toFloat() ?: 0f + } + + private fun mapLineCap(type: RNLineCapType?): Cap = when (type) { RNLineCapType.ROUND -> RoundCap() RNLineCapType.SQUARE -> SquareCap() else -> ButtCap() } - fun mapLineJoin(type: RNLineJoinType?): Int = + private fun mapLineJoin(type: RNLineJoinType?): Int = when (type) { RNLineJoinType.ROUND -> JointType.ROUND RNLineJoinType.BEVEL -> JointType.BEVEL diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 572b1ba..173a918 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -2,18 +2,14 @@ package com.rngooglemapsplus import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.bridge.UiThreadUtil -import com.facebook.react.uimanager.PixelUtil.dpToPx import com.facebook.react.uimanager.ThemedReactContext -import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.MapStyleOptions import com.margelo.nitro.core.Promise import com.rngooglemapsplus.extensions.circleEquals import com.rngooglemapsplus.extensions.markerEquals -import com.rngooglemapsplus.extensions.markerStyleEquals import com.rngooglemapsplus.extensions.polygonEquals import com.rngooglemapsplus.extensions.polylineEquals import com.rngooglemapsplus.extensions.toCameraPosition -import com.rngooglemapsplus.extensions.toColor import com.rngooglemapsplus.extensions.toMapColorScheme @DoNotStrip @@ -35,6 +31,8 @@ class RNGoogleMapsPlusView( override var initialProps: RNInitialProps? = null set(value) { + if (field == value) return + field = value view.initMapView( value?.mapId, value?.liteMode, @@ -44,31 +42,43 @@ class RNGoogleMapsPlusView( override var uiSettings: RNMapUiSettings? = null set(value) { + if (field == value) return + field = value view.uiSettings = value } override var myLocationEnabled: Boolean? = null set(value) { + if (field == value) return + field = value view.myLocationEnabled = value } override var buildingEnabled: Boolean? = null set(value) { + if (field == value) return + field = value view.buildingEnabled = value } override var trafficEnabled: Boolean? = null set(value) { + if (field == value) return + field = value view.trafficEnabled = value } override var indoorEnabled: Boolean? = null set(value) { + if (field == value) return + field = value view.indoorEnabled = value } override var customMapStyle: String? = null set(value) { + if (field == value) return + field = value currentCustomMapStyle = value value?.let { view.customMapStyle = MapStyleOptions(it) @@ -77,26 +87,29 @@ class RNGoogleMapsPlusView( override var userInterfaceStyle: RNUserInterfaceStyle? = null set(value) { + if (field == value) return + field = value view.userInterfaceStyle = value.toMapColorScheme() } - override var minZoomLevel: Double? = null - set(value) { - view.minZoomLevel = value - } - - override var maxZoomLevel: Double? = null + override var mapZoomConfig: RNMapZoomConfig? = null set(value) { - view.maxZoomLevel = value + if (field == value) return + field = value + view.mapZoomConfig = value } override var mapPadding: RNMapPadding? = null set(value) { + if (field == value) return + field = value view.mapPadding = value } override var mapType: RNMapType? = null set(value) { + if (field == value) return + field = value value?.let { view.mapType = it.value } @@ -104,8 +117,10 @@ class RNGoogleMapsPlusView( override var markers: Array? = null set(value) { + if (field.contentEquals(value)) return val prevById = field?.associateBy { it.id } ?: emptyMap() val nextById = value?.associateBy { it.id } ?: emptyMap() + field = value (prevById.keys - nextById.keys).forEach { id -> markerBuilder.cancelIconJob(id) @@ -122,38 +137,21 @@ class RNGoogleMapsPlusView( ) } } else if (!prev.markerEquals(next)) { - view.updateMarker(id) { m -> + view.updateMarker(id) { marker -> onUi { - m.position = - LatLng( - next.coordinate.latitude, - next.coordinate.longitude, - ) - next.zIndex?.let { m.zIndex = it.toFloat() } ?: run { - m.zIndex = 0f - } - - if (!prev.markerStyleEquals(next)) { - markerBuilder.buildIconAsync(id, next) { icon -> - m.setIcon(icon) - } - } - m.setAnchor( - (next.anchor?.x ?: 0.5).toFloat(), - (next.anchor?.y ?: 0.5).toFloat(), - ) + markerBuilder.update(marker, next, prev) } } } } - field = value } override var polylines: Array? = null set(value) { + if (field.contentEquals(value)) return val prevById = field?.associateBy { it.id } ?: emptyMap() val nextById = value?.associateBy { it.id } ?: emptyMap() - + field = value (prevById.keys - nextById.keys).forEach { id -> view.removePolyline(id) } @@ -161,35 +159,23 @@ class RNGoogleMapsPlusView( nextById.forEach { (id, next) -> val prev = prevById[id] if (prev == null) { - view.addPolyline(id, polylineBuilder.buildPolylineOptions(next)) + view.addPolyline(id, polylineBuilder.build(next)) } else if (!prev.polylineEquals(next)) { - view.updatePolyline(id) { gms -> + view.updatePolyline(id) { polyline -> onUi { - gms.points = - next.coordinates.map { - - LatLng(it.latitude, it.longitude) - } - next.width?.let { gms.width = it.dpToPx() } - next.lineCap?.let { - val cap = polylineBuilder.mapLineCap(it) - gms.startCap = cap - gms.endCap = cap - } - next.lineJoin?.let { gms.jointType = polylineBuilder.mapLineJoin(it) } - next.color?.let { gms.color = it.toColor() } - next.zIndex?.let { gms.zIndex = it.toFloat() } + polylineBuilder.update(polyline, next) } } } } - field = value } override var polygons: Array? = null set(value) { + if (field.contentEquals(value)) return val prevById = field?.associateBy { it.id } ?: emptyMap() val nextById = value?.associateBy { it.id } ?: emptyMap() + field = value (prevById.keys - nextById.keys).forEach { id -> view.removePolygon(id) @@ -198,30 +184,21 @@ class RNGoogleMapsPlusView( nextById.forEach { (id, next) -> val prev = prevById[id] if (prev == null) { - view.addPolygon(id, polygonBuilder.buildPolygonOptions(next)) + view.addPolygon(id, polygonBuilder.build(next)) } else if (!prev.polygonEquals(next)) { - view.updatePolygon(id) { gmsPoly -> - onUi { - gmsPoly.points = - next.coordinates.map { - com.google.android.gms.maps.model - .LatLng(it.latitude, it.longitude) - } - next.fillColor?.let { gmsPoly.fillColor = it.toColor() } - next.strokeColor?.let { gmsPoly.strokeColor = it.toColor() } - next.strokeWidth?.let { gmsPoly.strokeWidth = it.dpToPx() } - next.zIndex?.let { gmsPoly.zIndex = it.toFloat() } - } + view.updatePolygon(id) { polygon -> + onUi { polygonBuilder.update(polygon, next) } } } } - field = value } override var circles: Array? = null set(value) { + if (field.contentEquals(value)) return val prevById = field?.associateBy { it.id } ?: emptyMap() val nextById = value?.associateBy { it.id } ?: emptyMap() + field = value (prevById.keys - nextById.keys).forEach { id -> view.removeCircle(id) @@ -230,25 +207,21 @@ class RNGoogleMapsPlusView( nextById.forEach { (id, next) -> val prev = prevById[id] if (prev == null) { - view.addCircle(id, circleBuilder.buildCircleOptions(next)) + view.addCircle(id, circleBuilder.build(next)) } else if (!prev.circleEquals(next)) { - view.updateCircle(id) { gmsCircle -> + view.updateCircle(id) { circle -> onUi { - gmsCircle.center = LatLng(next.center.latitude, next.center.longitude) - next.radius?.let { gmsCircle.radius = it } - next.strokeWidth?.let { gmsCircle.strokeWidth = it.dpToPx() } - next.strokeColor?.let { gmsCircle.strokeColor = it.toColor() } - next.fillColor?.let { gmsCircle.fillColor = it.toColor() } - next.zIndex?.let { gmsCircle.zIndex = it.toFloat() } ?: run { gmsCircle.zIndex = 0f } + circleBuilder.update(circle, next) } } } } - field = value } override var locationConfig: RNLocationConfig? = null set(value) { + if (field == value) return + field = value view.locationConfig = value } @@ -261,6 +234,7 @@ class RNGoogleMapsPlusView( set(cb) { view.onMapReady = cb } + override var onLocationUpdate: ((RNLocation) -> Unit)? = null set(cb) { view.onLocationUpdate = cb diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index aae64d8..6f23948 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2356,7 +2356,7 @@ PODS: - React-perflogger (= 0.82.0-rc.4) - React-utils (= 0.82.0-rc.4) - SocketRocket - - RNGoogleMapsPlus (1.1.0-dev.1): + - RNGoogleMapsPlus (1.1.0-dev.2): - boost - DoubleConversion - fast_float @@ -2708,7 +2708,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 1202b833d8cca6c917dabcf679837c34a9ca5723 ReactCodegen: 19febbd1fdc8928493972f8d5290f2952e14c9d2 ReactCommon: 2caf7281b37aa1ca389e18839dd594099efb1489 - RNGoogleMapsPlus: 45deb9a3ba72d6557af3bfbf970f51de523364a1 + RNGoogleMapsPlus: 4f8dd1f33c906f62e877f1aa65cd8ac5ec6b020b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SVGKit: 1ad7513f8c74d9652f94ed64ddecda1a23864dea Yoga: 2fb906b2084fd388a52edae73c54c39c3f50e86c diff --git a/example/src/App.tsx b/example/src/App.tsx index 5052dc0..6b858a0 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -263,11 +263,60 @@ export const makeMarker = (id: number): RNMarker => ({ export default function App() { const mapRef = useRef(null); - const [show, setShow] = useState(false); const [stressTest, setStressTest] = useState(false); const [normalStyle, setNormalStyle] = useState(true); const [controlButtonsExpanded, setControlButtonsExpanded] = useState(false); + const [initialProps] = useState({ + /// mapStyle not working with mapId + /// mapId: '111', + camera: { + center: { + latitude: 37.7749, + longitude: -122.4194, + }, + zoom: 15, + }, + }); + + const [uiSettings] = useState({ + allGesturesEnabled: true, + compassEnabled: true, + indoorLevelPickerEnabled: true, + mapToolbarEnabled: true, + myLocationButtonEnabled: true, + rotateEnabled: true, + scrollEnabled: true, + scrollDuringRotateOrZoomEnabled: true, + tiltEnabled: true, + zoomControlsEnabled: true, + zoomGesturesEnabled: true, + }); + + const [mapPadding] = useState({ + top: 20, + left: 20, + bottom: 20, + right: 20, + }); + + const [mapZoomConfig] = useState({ + min: 0, + max: 20, + }); + + const [locationConfig] = useState({ + android: { + priority: RNAndroidLocationPriority.PRIORITY_BALANCED_POWER_ACCURACY, + interval: 5000, + minUpdateInterval: 5000, + }, + ios: { + desiredAccuracy: RNIOSLocationAccuracy.ACCURACY_BEST, + distanceFilterMeters: 10, + }, + }); + const [markers, setMaker] = useState( Array.from({ length: 0 }, (_, i) => makeMarker(i + 1)) ); @@ -285,26 +334,30 @@ export default function App() { ); useEffect(() => { - const interval = setInterval(() => { - if (stressTest) { - setMaker((m) => { - let newMarkers = [...m]; + if (!stressTest) return; - while (newMarkers.length > 100) { - newMarkers.shift(); - } - - for (let i = 0; i < 500; i++) { - newMarkers.push(makeMarker(newMarkers.length + 1)); - } + const interval = setInterval(() => { + setMaker((m) => { + const newMarkers = [...m]; + while (newMarkers.length > 100) { + newMarkers.shift(); + } + for (let i = 0; i < 500; i++) { + newMarkers.push(makeMarker(newMarkers.length + 1)); + } - return newMarkers; - }); - } + return newMarkers; + }); }, 100); + return () => clearInterval(interval); }, [stressTest]); + const mapStyle = useMemo( + () => JSON.stringify(normalStyle ? standardMapStyle : silverMapStyle), + [normalStyle] + ); + const buttons = useMemo(() => { return [ { @@ -328,10 +381,6 @@ export default function App() { ); }, }, - { - title: `${show ? 'Hide' : 'Show'} Marker`, - onPress: () => setShow(!show), - }, { title: `${stressTest ? 'Stop' : 'Start'} stress test`, onPress: () => setStressTest(!stressTest), @@ -367,7 +416,7 @@ export default function App() { console.log(mapRef.current?.isGooglePlayServicesAvailable()), }, ]; - }, [markers, normalStyle, show, stressTest]); + }, [markers, normalStyle, stressTest]); return ( @@ -377,61 +426,20 @@ export default function App() { mapRef.current = ref; }, }} - initialProps={{ - /// mapStyle not working with mapId - /// mapId: '111', - camera: { - center: { - latitude: 37.7749, - longitude: -122.4194, - }, - zoom: 15, - }, - }} - uiSettings={{ - allGesturesEnabled: true, - compassEnabled: true, - indoorLevelPickerEnabled: true, - mapToolbarEnabled: true, - myLocationButtonEnabled: true, - rotateEnabled: true, - scrollEnabled: true, - scrollDuringRotateOrZoomEnabled: true, - tiltEnabled: true, - zoomControlsEnabled: true, - zoomGesturesEnabled: true, - }} + initialProps={initialProps} + uiSettings={uiSettings} onMapReady={callback((ready) => console.log('Map is ready! ' + ready))} style={styles.map} myLocationEnabled={true} buildingEnabled={true} trafficEnabled={true} indoorEnabled={true} - customMapStyle={JSON.stringify( - normalStyle ? standardMapStyle : silverMapStyle - )} + customMapStyle={mapStyle} userInterfaceStyle={'light'} mapType={'normal'} - maxZoomLevel={20} - minZoomLevel={0} - mapPadding={{ - top: 20, - left: 20, - bottom: 20, - right: 20, - }} - locationConfig={{ - android: { - priority: - RNAndroidLocationPriority.PRIORITY_BALANCED_POWER_ACCURACY, - interval: 5000, - minUpdateInterval: 5000, - }, - ios: { - desiredAccuracy: RNIOSLocationAccuracy.ACCURACY_BEST, - distanceFilterMeters: 10, - }, - }} + mapZoomConfig={mapZoomConfig} + mapPadding={mapPadding} + locationConfig={locationConfig} onMapPress={{ f: function (coordinate: RNLatLng): void { console.log('Map pressed', coordinate); @@ -494,10 +502,10 @@ export default function App() { console.log('Location error:', error); }, }} - markers={show ? [...markers] : []} - polygons={show ? polygons : []} - polylines={show ? polylines : []} - circles={show ? circles : []} + markers={markers} + polygons={polygons} + polylines={polylines} + circles={circles} /> @@ -507,7 +515,7 @@ export default function App() { activeOpacity={0.8} > - {controlButtonsExpanded ? '▼ Hide Controls' : '▶ Show Controls'} + {controlButtonsExpanded ? 'Hide Controls' : 'Show Controls'} diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 1cc79fa..f1b12ef 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -60,22 +60,16 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { initialized = true let options = GMSMapViewOptions() options.frame = bounds - if let mapId = mapId { - options.mapID = GMSMapID(identifier: mapId) - } - if let liteMode = liteMode { - /// not supported - } - if let camera = camera { - options.camera = camera - } + + mapId.map { options.mapID = GMSMapID(identifier: $0) } + liteMode.map { _ in /* not supported */ } + camera.map { options.camera = $0 } + mapView = GMSMapView.init(options: options) mapView?.delegate = self mapView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] mapView?.paddingAdjustmentBehavior = .never - if let mapView = mapView { - addSubview(mapView) - } + mapView.map { addSubview($0) } initLocationCallbacks() applyPending() onMapReady?(true) @@ -109,115 +103,73 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { @MainActor private func applyPending() { - - if let padding = mapPadding { + mapPadding.map { mapView?.padding = UIEdgeInsets( - top: padding.top, - left: padding.left, - bottom: padding.bottom, - right: padding.right + top: $0.top, + left: $0.left, + bottom: $0.bottom, + right: $0.right ) } - if let uiSettings = uiSettings { - if let allGesturesEnabled = uiSettings.allGesturesEnabled { - mapView?.settings.setAllGesturesEnabled(allGesturesEnabled) - } - if let compassEnabled = uiSettings.compassEnabled { - mapView?.settings.compassButton = compassEnabled - } - if let indoorLevelPickerEnabled = uiSettings.indoorLevelPickerEnabled { - mapView?.settings.indoorPicker = indoorLevelPickerEnabled - } - if let mapToolbarEnabled = uiSettings.mapToolbarEnabled { - /// not supported - } - if let myLocationButtonEnabled = uiSettings.myLocationButtonEnabled { - mapView?.settings.myLocationButton = myLocationButtonEnabled - } - if let rotateEnabled = uiSettings.rotateEnabled { - mapView?.settings.rotateGestures = rotateEnabled - } - if let scrollEnabled = uiSettings.scrollEnabled { - mapView?.settings.scrollGestures = scrollEnabled - } - if let scrollDuringRotateOrZoomEnabled = uiSettings - .scrollDuringRotateOrZoomEnabled { - mapView?.settings.allowScrollGesturesDuringRotateOrZoom = - scrollDuringRotateOrZoomEnabled - } - if let tiltEnabled = uiSettings.tiltEnabled { - mapView?.settings.tiltGestures = tiltEnabled + if let v = uiSettings { + v.allGesturesEnabled.map { mapView?.settings.setAllGesturesEnabled($0) } + v.compassEnabled.map { mapView?.settings.compassButton = $0 } + v.indoorLevelPickerEnabled.map { mapView?.settings.indoorPicker = $0 } + v.mapToolbarEnabled.map { _ in /* not supported */ } + v.myLocationButtonEnabled.map { mapView?.settings.myLocationButton = $0 } + v.rotateEnabled.map { mapView?.settings.rotateGestures = $0 } + v.scrollEnabled.map { mapView?.settings.scrollGestures = $0 } + v.scrollDuringRotateOrZoomEnabled.map { + mapView?.settings.allowScrollGesturesDuringRotateOrZoom = $0 } - if let zoomControlsEnabled = uiSettings.zoomControlsEnabled { - /// not supported - } - if let zoomGesturesEnabled = uiSettings.zoomGesturesEnabled { - mapView?.settings.zoomGestures = zoomGesturesEnabled - } - } - - if let myLocation = myLocationEnabled { - mapView?.isMyLocationEnabled = myLocation - } - - if let buildings = buildingEnabled { - mapView?.isBuildingsEnabled = buildings - } - - if let traffic = trafficEnabled { - mapView?.isTrafficEnabled = traffic - } - - if let indoor = indoorEnabled { - mapView?.isIndoorEnabled = indoor - } - - if let style = customMapStyle { - mapView?.mapStyle = style - } - - if let mapType = mapType { - mapView?.mapType = mapType - } - - if let uiStyle = userInterfaceStyle { - mapView?.overrideUserInterfaceStyle = uiStyle - } - - if let minZoom = minZoomLevel, let maxZoom = maxZoomLevel { - mapView?.setMinZoom(Float(minZoom), maxZoom: Float(maxZoom)) + v.tiltEnabled.map { mapView?.settings.tiltGestures = $0 } + v.zoomControlsEnabled.map { _ in /* not supported */ } + v.zoomGesturesEnabled.map { mapView?.settings.zoomGestures = $0 } + } + + myLocationEnabled.map { mapView?.isMyLocationEnabled = $0 } + buildingEnabled.map { mapView?.isBuildingsEnabled = $0 } + trafficEnabled.map { mapView?.isTrafficEnabled = $0 } + indoorEnabled.map { mapView?.isIndoorEnabled = $0 } + customMapStyle.map { mapView?.mapStyle = $0 } + mapType.map { mapView?.mapType = $0 } + userInterfaceStyle.map { mapView?.overrideUserInterfaceStyle = $0 } + + mapZoomConfig.map { + mapView?.setMinZoom( + Float($0.min ?? 2), + maxZoom: Float($0.max ?? 21) + ) } - if let locationConfig = locationConfig { + locationConfig.map { locationHandler.desiredAccuracy = - locationConfig.ios?.desiredAccuracy?.toCLLocationAccuracy - locationHandler.distanceFilterMeters = - locationConfig.ios?.distanceFilterMeters + $0.ios?.desiredAccuracy?.toCLLocationAccuracy + locationHandler.distanceFilterMeters = $0.ios?.distanceFilterMeters } if !pendingMarkers.isEmpty { - pendingMarkers.forEach { - addMarkerInternal(id: $0.id, marker: $0.marker) - } + pendingMarkers.forEach { addMarkerInternal(id: $0.id, marker: $0.marker) } pendingMarkers.removeAll() } + if !pendingPolylines.isEmpty { pendingPolylines.forEach { addPolylineInternal(id: $0.id, polyline: $0.polyline) } pendingPolylines.removeAll() } + if !pendingPolygons.isEmpty { pendingPolygons.forEach { addPolygonInternal(id: $0.id, polygon: $0.polygon) } pendingPolygons.removeAll() } + if !pendingCircles.isEmpty { - pendingCircles.forEach { - addCircleInternal(id: $0.id, circle: $0.circle) - } + pendingCircles.forEach { addCircleInternal(id: $0.id, circle: $0.circle) } pendingCircles.removeAll() } } @@ -229,152 +181,86 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { @MainActor var uiSettings: RNMapUiSettings? { didSet { - guard let mapView = mapView else { return } - let settings = mapView.settings - - if let v = uiSettings { - if let allGesturesEnabled = v.allGesturesEnabled { - settings.setAllGesturesEnabled(allGesturesEnabled) - } - if let compassEnabled = v.compassEnabled { - settings.compassButton = compassEnabled - } - if let indoorLevelPickerEnabled = v.indoorLevelPickerEnabled { - settings.indoorPicker = indoorLevelPickerEnabled - } - if let mapToolbarEnabled = v.mapToolbarEnabled { - /// not supported - } - if let myLocationButtonEnabled = v.myLocationButtonEnabled { - settings.myLocationButton = myLocationButtonEnabled - } - if let rotateEnabled = v.rotateEnabled { - settings.rotateGestures = rotateEnabled - } - if let scrollEnabled = v.scrollEnabled { - settings.scrollGestures = scrollEnabled - } - if let scrollDuringRotateOrZoomEnabled = v - .scrollDuringRotateOrZoomEnabled { - settings.allowScrollGesturesDuringRotateOrZoom = - scrollDuringRotateOrZoomEnabled - } - if let tiltEnabled = v.tiltEnabled { - settings.tiltGestures = tiltEnabled - } - if let zoomControlsEnabled = v.zoomControlsEnabled { - /// not supported - } - if let zoomGesturesEnabled = v.zoomGesturesEnabled { - settings.zoomGestures = zoomGesturesEnabled - } - } else { - settings.setAllGesturesEnabled(true) - settings.compassButton = false - settings.indoorPicker = false - settings.myLocationButton = false - settings.rotateGestures = true - settings.scrollGestures = true - settings.allowScrollGesturesDuringRotateOrZoom = true - settings.tiltGestures = true - settings.zoomGestures = false - } + mapView?.settings.setAllGesturesEnabled( + uiSettings?.allGesturesEnabled ?? true + ) + mapView?.settings.compassButton = uiSettings?.compassEnabled ?? false + mapView?.settings.indoorPicker = + uiSettings?.indoorLevelPickerEnabled ?? false + mapView?.settings.myLocationButton = + uiSettings?.myLocationButtonEnabled ?? false + mapView?.settings.rotateGestures = uiSettings?.rotateEnabled ?? true + mapView?.settings.scrollGestures = uiSettings?.scrollEnabled ?? true + mapView?.settings.allowScrollGesturesDuringRotateOrZoom = + uiSettings?.scrollDuringRotateOrZoomEnabled ?? true + mapView?.settings.tiltGestures = uiSettings?.tiltEnabled ?? true + mapView?.settings.zoomGestures = uiSettings?.zoomGesturesEnabled ?? false } } @MainActor var myLocationEnabled: Bool? { didSet { - if let value = myLocationEnabled { - mapView?.isMyLocationEnabled = value - } else { - mapView?.isMyLocationEnabled = false - } + mapView?.isMyLocationEnabled = myLocationEnabled ?? false } } @MainActor var buildingEnabled: Bool? { didSet { - if let value = buildingEnabled { - mapView?.isBuildingsEnabled = value - } else { - mapView?.isBuildingsEnabled = false - } + mapView?.isBuildingsEnabled = buildingEnabled ?? false } } @MainActor var trafficEnabled: Bool? { didSet { - if let value = trafficEnabled { - mapView?.isTrafficEnabled = value - } else { - mapView?.isTrafficEnabled = false - } + mapView?.isTrafficEnabled = false } } @MainActor var indoorEnabled: Bool? { didSet { - if let value = indoorEnabled { - mapView?.isIndoorEnabled = value - } else { - mapView?.isIndoorEnabled = false - } + mapView?.isIndoorEnabled = indoorEnabled ?? false } } @MainActor var customMapStyle: GMSMapStyle? { didSet { - if let style = customMapStyle { - mapView?.mapStyle = style - } + mapView?.mapStyle = customMapStyle } } @MainActor var userInterfaceStyle: UIUserInterfaceStyle? { didSet { - if let style = userInterfaceStyle { - mapView?.overrideUserInterfaceStyle = style - } + mapView?.overrideUserInterfaceStyle = userInterfaceStyle ?? .unspecified } } @MainActor - var minZoomLevel: Double? { + var mapZoomConfig: RNMapZoomConfig? { didSet { - if let min = minZoomLevel, let max = maxZoomLevel { - mapView?.setMinZoom(Float(min), maxZoom: Float(max)) - } - } - } - - @MainActor - var maxZoomLevel: Double? { - didSet { - if let max = maxZoomLevel, let min = minZoomLevel { - mapView?.setMinZoom(Float(min), maxZoom: Float(max)) - } + mapView?.setMinZoom( + Float(mapZoomConfig?.min ?? 2), + maxZoom: Float(mapZoomConfig?.max ?? 21) + ) } } - @MainActor - var mapPadding: RNMapPadding? { + @MainActor var mapPadding: RNMapPadding? { didSet { - if let padding = mapPadding { - mapView?.padding = UIEdgeInsets( - top: padding.top, - left: padding.left, - bottom: padding.bottom, - right: padding.right - ) - } else { - mapView?.padding = .zero - } + mapView?.padding = + mapPadding.map { + UIEdgeInsets( + top: $0.top, + left: $0.left, + bottom: $0.bottom, + right: $0.right + ) + } ?? .zero } } @@ -475,7 +361,7 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { pendingMarkers.append((id, marker)) return } - if let old = markersById.removeValue(forKey: id) { old.map = nil } + markersById.removeValue(forKey: id).map { $0.map = nil } addMarkerInternal(id: id, marker: marker) } @@ -488,13 +374,12 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { @MainActor func updateMarker(id: String, block: @escaping (GMSMarker) -> Void) { - guard let m = markersById[id] else { return } - block(m) + markersById[id].map { block($0) } } @MainActor func removeMarker(id: String) { - if let m = markersById.removeValue(forKey: id) { m.map = nil } + markersById.removeValue(forKey: id).map { $0.map = nil } } @MainActor @@ -510,7 +395,7 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { pendingPolylines.append((id, polyline)) return } - if let old = polylinesById.removeValue(forKey: id) { old.map = nil } + polylinesById.removeValue(forKey: id).map { $0.map = nil } addPolylineInternal(id: id, polyline: polyline) } @@ -523,13 +408,12 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { @MainActor func updatePolyline(id: String, block: @escaping (GMSPolyline) -> Void) { - guard let pl = polylinesById[id] else { return } - block(pl) + polylinesById[id].map { block($0) } } @MainActor func removePolyline(id: String) { - if let pl = polylinesById.removeValue(forKey: id) { pl.map = nil } + polylinesById.removeValue(forKey: id).map { $0.map = nil } } @MainActor @@ -545,7 +429,7 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { pendingPolygons.append((id, polygon)) return } - if let old = polygonsById.removeValue(forKey: id) { old.map = nil } + polygonsById.removeValue(forKey: id).map { $0.map = nil } addPolygonInternal(id: id, polygon: polygon) } @@ -558,13 +442,12 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { @MainActor func updatePolygon(id: String, block: @escaping (GMSPolygon) -> Void) { - guard let pg = polygonsById[id] else { return } - block(pg) + polygonsById[id].map { block($0) } } @MainActor func removePolygon(id: String) { - if let pg = polygonsById.removeValue(forKey: id) { pg.map = nil } + polygonsById.removeValue(forKey: id).map { $0.map = nil } } @MainActor @@ -580,7 +463,7 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { pendingCircles.append((id, circle)) return } - if let old = circlesById.removeValue(forKey: id) { old.map = nil } + circlesById.removeValue(forKey: id).map { $0.map = nil } addCircleInternal(id: id, circle: circle) } @@ -593,13 +476,12 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { @MainActor func updateCircle(id: String, block: @escaping (GMSCircle) -> Void) { - guard let circle = circlesById[id] else { return } - block(circle) + circlesById[id].map { block($0) } } @MainActor func removeCircle(id: String) { - if let circle = circlesById.removeValue(forKey: id) { circle.map = nil } + circlesById.removeValue(forKey: id).map { $0.map = nil } } @MainActor diff --git a/ios/LocationHandler.swift b/ios/LocationHandler.swift index 176cf5d..3ebe414 100644 --- a/ios/LocationHandler.swift +++ b/ios/LocationHandler.swift @@ -13,21 +13,13 @@ final class LocationHandler: NSObject, CLLocationManagerDelegate { var desiredAccuracy: CLLocationAccuracy? = kCLLocationAccuracyDefault { didSet { - if let desiredAccuracy = desiredAccuracy { - manager.desiredAccuracy = desiredAccuracy - } else { - manager.desiredAccuracy = kCLLocationAccuracyBest - } + manager.desiredAccuracy = desiredAccuracy ?? kCLLocationAccuracyBest } } var distanceFilterMeters: CLLocationDistance? = kCLDistanceFilterNoneDefault { didSet { - if let distanceFilterMeters = distanceFilterMeters { - manager.distanceFilter = distanceFilterMeters - } else { - manager.distanceFilter = kCLDistanceFilterNone - } + manager.distanceFilter = distanceFilterMeters ?? kCLDistanceFilterNone } } diff --git a/ios/MapCircleBuilder.swift b/ios/MapCircleBuilder.swift index 7dc9fe0..729c7f6 100644 --- a/ios/MapCircleBuilder.swift +++ b/ios/MapCircleBuilder.swift @@ -1,20 +1,35 @@ import GoogleMaps final class MapCircleBuilder { - - func buildCircle(_ c: RNCircle) -> GMSCircle { + func build(_ c: RNCircle) -> GMSCircle { let circle = GMSCircle() circle.position = CLLocationCoordinate2D( latitude: c.center.latitude, longitude: c.center.longitude ) - if let r = c.radius { circle.radius = r } - if let fc = c.fillColor?.toUIColor() { circle.fillColor = fc } - if let sc = c.strokeColor?.toUIColor() { circle.strokeColor = sc } - if let sw = c.strokeWidth { circle.strokeWidth = CGFloat(sw) } - if let pr = c.pressable { circle.isTappable = pr } - if let zi = c.zIndex { circle.zIndex = Int32(zi) } + + circle.radius = c.radius + c.fillColor.map { circle.fillColor = $0.toUIColor() } + c.strokeColor.map { circle.strokeColor = $0.toUIColor() } + c.strokeWidth.map { circle.strokeWidth = CGFloat($0) } + c.pressable.map { circle.isTappable = $0 } + c.zIndex.map { circle.zIndex = Int32($0) } return circle } + + func update(_ next: RNCircle, _ c: GMSCircle) { + c.position = CLLocationCoordinate2D( + latitude: next.center.latitude, + longitude: next.center.longitude + ) + + c.radius = next.radius + c.fillColor = next.fillColor?.toUIColor() ?? nil + c.strokeColor = next.strokeColor?.toUIColor() ?? .black + c.strokeWidth = CGFloat(next.strokeWidth ?? 1.0) + c.isTappable = next.pressable ?? false + c.zIndex = Int32(next.zIndex ?? 0) + } + } diff --git a/ios/MapHelper.swift b/ios/MapHelper.swift index 7623522..b8ebaf0 100644 --- a/ios/MapHelper.swift +++ b/ios/MapHelper.swift @@ -9,10 +9,12 @@ func withCATransaction( _ body: () -> Void ) { CATransaction.begin() - if disableActions { CATransaction.setDisableActions(true) } - if let d = duration { CATransaction.setAnimationDuration(d) } - if let tf = timingFunction { CATransaction.setAnimationTimingFunction(tf) } - if let c = completion { CATransaction.setCompletionBlock(c) } + + CATransaction.setDisableActions(disableActions) + duration.map { CATransaction.setAnimationDuration($0) } + timingFunction.map { CATransaction.setAnimationTimingFunction($0) } + completion.map { CATransaction.setCompletionBlock($0) } + body() CATransaction.commit() } diff --git a/ios/MapMarkerBuilder.swift b/ios/MapMarkerBuilder.swift index 4ee13be..98e2fe6 100644 --- a/ios/MapMarkerBuilder.swift +++ b/ios/MapMarkerBuilder.swift @@ -25,13 +25,49 @@ final class MapMarkerBuilder { x: m.anchor?.x ?? 0.5, y: m.anchor?.y ?? 0.5 ) - if let zi = m.zIndex { marker.zIndex = Int32(zi) } + + m.zIndex.map { marker.zIndex = Int32($0) } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak marker] in marker?.tracksViewChanges = false } + return marker } + @MainActor + func update(_ prev: RNMarker, _ next: RNMarker, _ m: GMSMarker) { + m.position = CLLocationCoordinate2D( + latitude: next.coordinate.latitude, + longitude: next.coordinate.longitude + ) + + m.zIndex = Int32(next.zIndex ?? 0) + + m.groundAnchor = CGPoint( + x: next.anchor?.x ?? 0.5, + y: next.anchor?.y ?? 0.5 + ) + + if !prev.markerStyleEquals(next) { + buildIconAsync(next.id, next) { img in + m.tracksViewChanges = true + m.icon = img + + if prev.anchor?.x != next.anchor?.x || prev.anchor?.y != next.anchor?.y { + m.groundAnchor = CGPoint( + x: next.anchor?.x ?? 0.5, + y: next.anchor?.y ?? 0.5 + ) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak m] in + m?.tracksViewChanges = false + } + } + } + } + @MainActor func buildIconAsync( _ id: String, @@ -53,14 +89,10 @@ final class MapMarkerBuilder { let task = Task(priority: .userInitiated) { [weak self] in guard let self else { return } - defer { - self.tasks.removeValue(forKey: id) - } + defer { self.tasks.removeValue(forKey: id) } let img = await self.renderUIImage(m) - guard let img, !Task.isCancelled else { - return - } + guard let img, !Task.isCancelled else { return } self.iconCache.setObject(img, forKey: key) @@ -68,44 +100,11 @@ final class MapMarkerBuilder { guard !Task.isCancelled else { return } onReady(img) } - } tasks[id] = task } - @MainActor - func updateMarker(_ prev: RNMarker, _ next: RNMarker, _ m: GMSMarker) { - m.position = CLLocationCoordinate2D( - latitude: next.coordinate.latitude, - longitude: next.coordinate.longitude - ) - - if let zi = next.zIndex { m.zIndex = Int32(zi) } - - m.groundAnchor = CGPoint( - x: next.anchor?.x ?? 0.5, - y: next.anchor?.y ?? 0.5 - ) - - if !prev.markerStyleEquals(next) { - buildIconAsync(next.id, next) { img in - m.tracksViewChanges = true - m.icon = img - if prev.anchor?.x != next.anchor?.x || prev.anchor?.y != next.anchor?.y { - m.groundAnchor = CGPoint( - x: next.anchor?.x ?? 0.5, - y: next.anchor?.y ?? 0.5 - ) - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak m] in - m?.tracksViewChanges = false - } - } - } - } - func cancelIconTask(_ id: String) { tasks[id]?.cancel() tasks.removeValue(forKey: id) @@ -181,5 +180,4 @@ final class MapMarkerBuilder { onCancel: {} ) } - } diff --git a/ios/MapPolygonBuilder.swift b/ios/MapPolygonBuilder.swift index c68c6b2..2dcd68e 100644 --- a/ios/MapPolygonBuilder.swift +++ b/ios/MapPolygonBuilder.swift @@ -1,20 +1,38 @@ import GoogleMaps final class MapPolygonBuilder { - - func buildPolygon(_ p: RNPolygon) -> GMSPolygon { + func build(_ p: RNPolygon) -> GMSPolygon { let path = GMSMutablePath() p.coordinates.forEach { path.add( CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) ) } + let pg = GMSPolygon(path: path) - if let fc = p.fillColor?.toUIColor() { pg.fillColor = fc } - if let sc = p.strokeColor?.toUIColor() { pg.strokeColor = sc } - if let sw = p.strokeWidth { pg.strokeWidth = CGFloat(sw) } - if let pr = p.pressable { pg.isTappable = pr } - if let zi = p.zIndex { pg.zIndex = Int32(zi) } + + p.fillColor.map { pg.fillColor = $0.toUIColor() } + p.strokeColor.map { pg.strokeColor = $0.toUIColor() } + p.strokeWidth.map { pg.strokeWidth = CGFloat($0) } + p.pressable.map { pg.isTappable = $0 } + p.zIndex.map { pg.zIndex = Int32($0) } + return pg } + + func update(_ next: RNPolygon, _ pg: GMSPolygon) { + let path = GMSMutablePath() + next.coordinates.forEach { + path.add( + CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) + ) + } + pg.path = path + + pg.fillColor = next.fillColor?.toUIColor() ?? .clear + pg.strokeColor = next.strokeColor?.toUIColor() ?? .black + pg.strokeWidth = CGFloat(next.strokeWidth ?? 1.0) + pg.isTappable = next.pressable ?? false + pg.zIndex = Int32(next.zIndex ?? 0) + } } diff --git a/ios/MapPolylineBuilder.swift b/ios/MapPolylineBuilder.swift index bbb0b9e..0fdf0a3 100644 --- a/ios/MapPolylineBuilder.swift +++ b/ios/MapPolylineBuilder.swift @@ -1,24 +1,40 @@ import GoogleMaps final class MapPolylineBuilder { - func buildPolyline(_ p: RNPolyline) -> GMSPolyline { + func build(_ p: RNPolyline) -> GMSPolyline { let path = GMSMutablePath() p.coordinates.forEach { path.add( CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) ) } + let pl = GMSPolyline(path: path) - if let w = p.width { pl.strokeWidth = CGFloat(w) } - if let c = p.color?.toUIColor() { pl.strokeColor = c } - if let cap = p.lineCap { - /// pl.lineCap = mapLineCap(cap) - } - if let join = p.lineJoin { - /// pl.strokeJoin = mapLineJoin(join) - } - if let pr = p.pressable { pl.isTappable = pr } - if let zi = p.zIndex { pl.zIndex = Int32(zi) } + + p.width.map { pl.strokeWidth = CGFloat($0) } + p.color.map { pl.strokeColor = $0.toUIColor() } + p.lineCap.map { _ in /* not supported */ } + p.lineJoin.map { _ in /* not supported */ } + p.pressable.map { pl.isTappable = $0 } + p.zIndex.map { pl.zIndex = Int32($0) } + return pl } + + func update(_ next: RNPolyline, _ pl: GMSPolyline) { + let path = GMSMutablePath() + next.coordinates.forEach { + path.add( + CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) + ) + } + pl.path = path + + /* lineCap not supported */ + /* lineJoin not supported */ + pl.strokeWidth = CGFloat(next.width ?? 1.0) + pl.strokeColor = next.color?.toUIColor() ?? .black + pl.isTappable = next.pressable ?? false + pl.zIndex = Int32(next.zIndex ?? 0) + } } diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index be6ed92..e7ce775 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -88,13 +88,8 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } @MainActor - var minZoomLevel: Double? { - didSet { impl.minZoomLevel = minZoomLevel } - } - - @MainActor - var maxZoomLevel: Double? { - didSet { impl.maxZoomLevel = maxZoomLevel } + var mapZoomConfig: RNMapZoomConfig? { + didSet { impl.mapZoomConfig = mapZoomConfig } } @MainActor @@ -135,7 +130,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { if let prev = prevById[id] { if !prev.markerEquals(next) { impl.updateMarker(id: id) { m in - self.markerBuilder.updateMarker(prev, next, m) + self.markerBuilder.update(prev, next, m) } } } else { @@ -168,13 +163,13 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { if let prev = prevById[id] { if !prev.polylineEquals(next) { impl.updatePolyline(id: id) { pl in - prev.updatePolyline(next, pl) + self.polylineBuilder.update(next, pl) } } } else { impl.addPolyline( id: id, - polyline: polylineBuilder.buildPolyline(next) + polyline: polylineBuilder.build(next) ) } } @@ -200,11 +195,11 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { if let prev = prevById[id] { if !prev.polygonEquals(next) { impl.updatePolygon(id: id) { pg in - prev.updatePolygon(next, pg) + self.polygonBuilder.update(next, pg) } } } else { - impl.addPolygon(id: id, polygon: polygonBuilder.buildPolygon(next)) + impl.addPolygon(id: id, polygon: polygonBuilder.build(next)) } } } @@ -229,11 +224,11 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { if let prev = prevById[id] { if !prev.circleEquals(next) { impl.updateCircle(id: id) { circle in - prev.updateCircle(next, circle) + self.circleBuilder.update(next, circle) } } } else { - impl.addCircle(id: id, circle: circleBuilder.buildCircle(next)) + impl.addCircle(id: id, circle: circleBuilder.build(next)) } } } diff --git a/ios/extensions/RNCircle+Extension.swift b/ios/extensions/RNCircle+Extension.swift index 7364b39..8d2557b 100644 --- a/ios/extensions/RNCircle+Extension.swift +++ b/ios/extensions/RNCircle+Extension.swift @@ -1,19 +1,6 @@ import GoogleMaps extension RNCircle { - func updateCircle(_ next: RNCircle, _ c: GMSCircle) { - c.position = CLLocationCoordinate2D( - latitude: next.center.latitude, - longitude: next.center.longitude - ) - if let r = next.radius { c.radius = r } - if let fc = next.fillColor?.toUIColor() { c.fillColor = fc } - if let sc = next.strokeColor?.toUIColor() { c.strokeColor = sc } - if let sw = next.strokeWidth { c.strokeWidth = CGFloat(sw) } - if let pr = next.pressable { c.isTappable = pr } - if let zi = next.zIndex { c.zIndex = Int32(zi) } - } - func circleEquals(_ b: RNCircle) -> Bool { zIndex == b.zIndex && pressable == b.pressable && center.latitude == b.center.latitude diff --git a/ios/extensions/RNPolygon+Extension.swift.swift b/ios/extensions/RNPolygon+Extension.swift.swift index c274024..7aee161 100644 --- a/ios/extensions/RNPolygon+Extension.swift.swift +++ b/ios/extensions/RNPolygon+Extension.swift.swift @@ -1,25 +1,6 @@ import GoogleMaps extension RNPolygon { - func updatePolygon(_ next: RNPolygon, _ pg: GMSPolygon) { - let path = GMSMutablePath() - next.coordinates.forEach { - path.add( - CLLocationCoordinate2D( - latitude: $0.latitude, - longitude: $0.longitude - ) - ) - } - pg.path = path - - if let fc = next.fillColor?.toUIColor() { pg.fillColor = fc } - if let sc = next.strokeColor?.toUIColor() { pg.strokeColor = sc } - if let sw = next.strokeWidth { pg.strokeWidth = CGFloat(sw) } - if let pr = next.pressable { pg.isTappable = pr } - if let zi = next.zIndex { pg.zIndex = Int32(zi) } - } - func polygonEquals(_ b: RNPolygon) -> Bool { guard zIndex == b.zIndex, pressable == b.pressable, @@ -28,12 +9,14 @@ extension RNPolygon { strokeColor == b.strokeColor, coordinates.count == b.coordinates.count else { return false } + for i in 0.. Bool { guard zIndex == b.zIndex, (width ?? 0) == (b.width ?? 0), @@ -35,6 +9,7 @@ extension RNPolyline { color == b.color, coordinates.count == b.coordinates.count else { return false } + for i in 0..