diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 46ab2ec..ad55a07 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -153,9 +153,9 @@ class GoogleMapsViewImpl( } initLocationCallbacks() applyPending() + mapReady = true + onMapReady?.invoke(true) } - mapReady = true - onMapReady?.invoke(true) } override fun onCameraMoveStarted(reason: Int) { @@ -372,6 +372,8 @@ class GoogleMapsViewImpl( } } + var initialProps: RNInitialProps? = null + var uiSettings: RNMapUiSettings? = null set(value) { field = value @@ -975,6 +977,7 @@ class GoogleMapsViewImpl( fun destroyInternal() { onUi { + locationHandler.stop() markerBuilder.cancelAllJobs() clearMarkers() clearPolylines() @@ -982,7 +985,6 @@ class GoogleMapsViewImpl( clearCircles() clearHeatmaps() clearKmlLayer() - locationHandler.stop() googleMap?.apply { setOnCameraMoveStartedListener(null) setOnCameraMoveListener(null) @@ -1003,6 +1005,7 @@ class GoogleMapsViewImpl( } super.removeAllViews() reactContext.removeLifecycleEventListener(this) + initialized = false } } diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 845421f..d31d24e 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -21,6 +21,7 @@ import com.rngooglemapsplus.extensions.toSize class RNGoogleMapsPlusView( val context: ThemedReactContext, ) : HybridRNGoogleMapsPlusViewSpec() { + private var propsInitialized = false private var currentCustomMapStyle: String? = null private var permissionHandler = PermissionHandler(context) private var locationHandler = LocationHandler(context) @@ -35,15 +36,23 @@ class RNGoogleMapsPlusView( override val view = GoogleMapsViewImpl(context, locationHandler, playServiceHandler, markerBuilder) + override fun afterUpdate() { + super.afterUpdate() + if (!propsInitialized) { + propsInitialized = true + view.initMapView( + initialProps?.mapId, + initialProps?.liteMode, + initialProps?.camera?.toCameraPosition(), + ) + } + } + override var initialProps: RNInitialProps? = null set(value) { if (field == value) return field = value - view.initMapView( - value?.mapId, - value?.liteMode, - value?.camera?.toCameraPosition(), - ) + view.initialProps = value } override var uiSettings: RNMapUiSettings? = null diff --git a/example/src/components/MapWrapper.tsx b/example/src/components/MapWrapper.tsx index a9cfb80..5ccb035 100644 --- a/example/src/components/MapWrapper.tsx +++ b/example/src/components/MapWrapper.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { StyleSheet, useColorScheme, View } from 'react-native'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; import { GoogleMapsView, type RNIndoorBuilding, @@ -22,6 +22,7 @@ import { import type { ViewProps } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { callback } from 'react-native-nitro-modules'; +import { useTheme } from '@react-navigation/native'; type Props = ViewProps & RNGoogleMapsPlusViewProps & { @@ -31,8 +32,11 @@ type Props = ViewProps & export default function MapWrapper(props: Props) { const { children, ...rest } = props; - const scheme = useColorScheme(); + const theme = useTheme(); + const styles = getThemedStyles(theme); const layout = useSafeAreaInsets(); + + const [mapReady, setMapReady] = React.useState(false); const initialProps = useMemo( () => ({ camera: { @@ -96,7 +100,7 @@ export default function MapWrapper(props: Props) { uiSettings={props.uiSettings ?? uiSettings} style={[styles.map, props.style]} userInterfaceStyle={ - props.userInterfaceStyle ?? (scheme === 'dark' ? 'dark' : 'light') + props.userInterfaceStyle ?? (theme.dark ? 'dark' : 'light') } mapType={props.mapType ?? 'normal'} mapZoomConfig={props.mapZoomConfig ?? mapZoomConfig} @@ -104,7 +108,10 @@ export default function MapWrapper(props: Props) { locationConfig={props.locationConfig ?? locationConfig} onMapReady={callback( props.onMapReady ?? { - f: (ready: boolean) => console.log('Map is ready! ' + ready), + f: (ready: boolean) => { + console.log('Map is ready! ' + ready); + setMapReady(true); + }, } )} onMapError={callback( @@ -197,17 +204,33 @@ export default function MapWrapper(props: Props) { )} /> {children} + {!mapReady && ( + + + + )} ); } -const styles = StyleSheet.create({ - container: { flex: 1 }, - map: { - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - }, -}); +const getThemedStyles = (theme: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + map: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + loadingOverlay: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + zIndex: 10, + backgroundColor: theme.dark ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.7)', + }, + }); diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml index 8d80b0f..a84aed4 100644 --- a/ios/.swiftlint.yml +++ b/ios/.swiftlint.yml @@ -4,7 +4,6 @@ disabled_rules: - cyclomatic_complexity - function_body_length - closure_parameter_position - - todo identifier_name: min_length: diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 11b4f29..4fcedc2 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -198,6 +198,11 @@ GMSIndoorDisplayDelegate { mapView?.camera } + @MainActor + var initialProps: RNInitialProps? { + didSet {} + } + @MainActor var uiSettings: RNMapUiSettings? { didSet { @@ -629,7 +634,10 @@ GMSIndoorDisplayDelegate { @MainActor func clearHeatmaps() { - heatmapsById.values.forEach { $0.map = nil } + heatmapsById.values.forEach { + $0.clearTileCache() + $0.map = nil + } heatmapsById.removeAll() pendingHeatmaps.removeAll() } @@ -671,16 +679,21 @@ GMSIndoorDisplayDelegate { } func deinitInternal() { - markerBuilder.cancelAllIconTasks() - clearMarkers() - clearPolylines() - clearPolygons() - clearCircles() - clearHeatmaps() - locationHandler.stop() - mapView?.clear() - mapView?.delegate = nil - mapView = nil + onMain { + self.locationHandler.stop() + self.markerBuilder.cancelAllIconTasks() + self.clearMarkers() + self.clearPolylines() + self.clearPolygons() + self.clearCircles() + self.clearHeatmaps() + self.clearKmlLayers() + self.mapView?.clear() + self.mapView?.indoorDisplay.delegate = nil + self.mapView?.delegate = nil + self.mapView = nil + self.initialized = false + } } @objc private func appDidBecomeActive() { @@ -696,9 +709,6 @@ GMSIndoorDisplayDelegate { override func didMoveToWindow() { super.didMoveToWindow() if window != nil { - if mapView != nil && mapReady { - onMapReady?(true) - } locationHandler.start() } else { locationHandler.stop() diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index cf7c0db..13b7e87 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -8,6 +8,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { private let permissionHandler: PermissionHandler private let locationHandler: LocationHandler + private var propsInitialized = false private let markerBuilder = MapMarkerBuilder() private let polylineBuilder = MapPolylineBuilder() private let polygonBuilder = MapPolygonBuilder() @@ -29,21 +30,23 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { ) } - /* - /// TODO: prepareForRecycle - override func prepareForRecycle() { - impl.clearAll() - } - */ + func afterUpdate() { + if !propsInitialized { + propsInitialized = true + Task { @MainActor in + impl.initMapView( + mapId: self.initialProps?.mapId, + liteMode: self.initialProps?.liteMode, + camera: self.initialProps?.camera?.toGMSCameraPosition(current: nil) + ) + } + } + } @MainActor var initialProps: RNInitialProps? { didSet { - impl.initMapView( - mapId: initialProps?.mapId, - liteMode: initialProps?.liteMode, - camera: initialProps?.camera?.toGMSCameraPosition(current: nil) - ) + impl.initialProps = initialProps } } diff --git a/scripts/nitrogen-patch.js b/scripts/nitrogen-patch.js index ce7832f..c4d5353 100644 --- a/scripts/nitrogen-patch.js +++ b/scripts/nitrogen-patch.js @@ -1,24 +1,44 @@ /** - * Recursively patches all generated Android files: + * Recursively patches all generated Nitro files (Android & iOS): + * + * ANDROID * - Replaces 'com.margelo.nitro.rngooglemapsplus' -> 'com.rngooglemapsplus' * - Replaces 'com/margelo/nitro/rngooglemapsplus' -> 'com/rngooglemapsplus' * - Removes 'margelo/nitro/' in RNGoogleMapsPlusOnLoad.cpp - * - Inserts `prepareToRecycleView()` under `onDropViewInstance()` if missing + * - Inserts `prepareToRecycleView()` + * nitrogen/generated/android/kotlin/com/margelo/nitro/rngooglemapsplus/views/HybridRNGoogleMapsPlusViewManager.kt + * + * iOS + * - Inserts `+ (BOOL)shouldBeRecycled` + * nitrogen/generated/ios/c++/views/HybridRNGoogleMapsPlusViewComponent.mm */ import { fileURLToPath } from 'url'; import { basename } from 'path'; import path from 'node:path'; import { readdir, readFile, writeFile } from 'node:fs/promises'; -const ROOT_DIR = path.join(process.cwd(), 'nitrogen', 'generated', 'android'); -console.log(ROOT_DIR); -const ANDROID_ONLOAD_FILE = path.join(ROOT_DIR, 'RNGoogleMapsPlusOnLoad.cpp'); +const ROOT_ANDROID = path.join( + process.cwd(), + 'nitrogen', + 'generated', + 'android' +); +const ROOT_IOS = path.join(process.cwd(), 'nitrogen', 'generated', 'ios'); +const ANDROID_ONLOAD_FILE = path.join( + ROOT_ANDROID, + 'RNGoogleMapsPlusOnLoad.cpp' +); const HYBRID_VIEW_MANAGER = path.join( - ROOT_DIR, + ROOT_ANDROID, 'kotlin/com/margelo/nitro/rngooglemapsplus/views/HybridRNGoogleMapsPlusViewManager.kt' ); +const HYBRID_VIEW_COMPONENT_IOS = path.join( + ROOT_IOS, + 'c++/views/HybridRNGoogleMapsPlusViewComponent.mm' +); + const REPLACEMENTS = [ { regex: /com\.margelo\.nitro\.rngooglemapsplus/g, @@ -33,14 +53,21 @@ const REPLACEMENTS = [ const __filename = fileURLToPath(import.meta.url); const filename = basename(__filename); -const RECYCLE_METHOD = ` +const RECYCLE_METHOD_ANDROID = ` /// added by ${filename} override fun prepareToRecycleView(reactContext: ThemedReactContext, view: View): View? { return null } `; -// Patch-Routine +const RECYCLE_METHOD_IOS = ` +/// added by ${filename} ++ (BOOL)shouldBeRecycled +{ + return NO; +} +`; + async function processFile(filePath) { let content = await readFile(filePath, 'utf8'); let updated = content; @@ -53,16 +80,30 @@ async function processFile(filePath) { updated = updated.replace(/margelo\/nitro\//g, ''); } - console.log(filePath); if (path.resolve(filePath) === path.resolve(HYBRID_VIEW_MANAGER)) { if (!/override fun prepareToRecycleView/.test(updated)) { const pattern = /(override fun onDropViewInstance\(view: View\)\s*\{[^}]+\}\s*)/m; if (pattern.test(updated)) { - updated = updated.replace(pattern, `$1${RECYCLE_METHOD}\n`); + updated = updated.replace(pattern, `$1${RECYCLE_METHOD_ANDROID}\n`); + } else { + throw new Error( + `Pattern for "onDropViewInstance" not found in ${filePath}` + ); + } + } + } + + if (path.resolve(filePath) === path.resolve(HYBRID_VIEW_COMPONENT_IOS)) { + if (!/\+\s*\(BOOL\)\s*shouldBeRecycled/.test(updated)) { + const pattern = + /(- \(instancetype\)\s*init\s*\{(?:[^{}]|\{[^{}]*\})*\})/m; + + if (pattern.test(updated)) { + updated = updated.replace(pattern, `$1\n${RECYCLE_METHOD_IOS}`); } else { - updated = updated.replace(/}\s*$/m, `${RECYCLE_METHOD}\n}\n`); + throw new Error(`Pattern for "init" not found in ${filePath}`); } } } @@ -87,8 +128,9 @@ async function start(dir) { (async () => { try { - await start(ROOT_DIR); - console.log('All occurrences patched successfully.'); + await start(ROOT_ANDROID); + await start(ROOT_IOS); + console.log('All Nitrogen files patched successfully.'); } catch (err) { console.error('Error while processing files:', err); process.exit(1);