Skip to content

Commit cc0aae1

Browse files
authored
fix: iOS navigation session attachment and detachment (#497)
1 parent d639fe4 commit cc0aae1

File tree

13 files changed

+183
-35
lines changed

13 files changed

+183
-35
lines changed

example/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
useNavigation as useAppNavigation,
2222
type NavigationProp,
2323
} from '@react-navigation/native';
24+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2425
import { createStackNavigator } from '@react-navigation/stack';
2526
import { View, Button, Text } from 'react-native';
2627
import { CommonStyles } from './styles/components';
@@ -48,6 +49,7 @@ export type StackNavigation = NavigationProp<RootStackParamList>;
4849
const HomeScreen = () => {
4950
const { navigate } = useAppNavigation<StackNavigation>();
5051
const isFocused = useIsFocused();
52+
const insets = useSafeAreaInsets();
5153
const [sdkVersion, setSdkVersion] = useState<string>('');
5254

5355
const { navigationController } = useNavigation();
@@ -67,7 +69,7 @@ const HomeScreen = () => {
6769
}, [navigationController]);
6870

6971
return (
70-
<View style={CommonStyles.centered}>
72+
<View style={[CommonStyles.centered, { paddingBottom: insets.bottom }]}>
7173
{/* SDK Version Display */}
7274
<View style={{ padding: 16, alignItems: 'center' }}>
7375
<Text style={{ fontSize: 16, fontWeight: 'bold', color: '#333' }}>

example/src/screens/IntegrationTestsScreen.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import React, { useState, useMemo, useCallback } from 'react';
1919
import { Button, Text, View } from 'react-native';
2020
import Snackbar from 'react-native-snackbar';
21+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2122

2223
import {
2324
type Circle,
@@ -80,6 +81,7 @@ const IntegrationTestsScreen = () => {
8081
const [failureMessage, setFailuremessage] = useState('');
8182
const [navigationViewController, setNavigationViewController] =
8283
useState<NavigationViewController | null>(null);
84+
const insets = useSafeAreaInsets();
8385

8486
const onMapReady = useCallback(async () => {
8587
console.log('Map is ready, initializing navigator...');
@@ -220,7 +222,7 @@ const IntegrationTestsScreen = () => {
220222
}, [testStatus, detoxStepNumber]);
221223

222224
return (
223-
<View style={CommonStyles.container}>
225+
<View style={[CommonStyles.container, { paddingBottom: insets.bottom }]}>
224226
<Text>See CONTRIBUTING.md to see how to run integration tests.</Text>
225227
<View style={{ flex: 6, margin: 5 }}>
226228
<NavigationView

example/src/screens/MapIdScreen.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
type NavigationViewCallbacks,
3535
useNavigation,
3636
} from '@googlemaps/react-native-navigation-sdk';
37+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
3738
import usePermissions from '../checkPermissions';
3839

3940
const MapIdScreen = () => {
@@ -43,7 +44,7 @@ const MapIdScreen = () => {
4344
useState<NavigationViewController | null>(null);
4445
const [navigationUiEnabled, setNavigationUIEnabled] = useState(true);
4546
const [nightMode, setNightMode] = useState<number>(0); // 0: Auto, 1: Force Day, 2: Force Night
46-
47+
const insets = useSafeAreaInsets();
4748
const { arePermissionsApproved } = usePermissions();
4849
const { navigationController, addListeners, removeListeners } =
4950
useNavigation();
@@ -172,7 +173,7 @@ const MapIdScreen = () => {
172173
return (
173174
<ScrollView
174175
style={CommonStyles.container}
175-
contentContainerStyle={styles.content}
176+
contentContainerStyle={[styles.content, { paddingBottom: insets.bottom }]}
176177
>
177178
{confirmedMapId === null ? (
178179
// Configuration screen

example/src/screens/MultipleMapsScreen.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import PagerView, {
2626
type PagerViewOnPageSelectedEvent,
2727
} from 'react-native-pager-view';
2828
import Snackbar from 'react-native-snackbar';
29+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2930

3031
import {
3132
NavigationInitErrorCode,
@@ -66,6 +67,7 @@ enum OverlayType {
6667
}
6768

6869
const MultipleMapsScreen = () => {
70+
const insets = useSafeAreaInsets();
6971
const [mapsVisible, setMapsVisible] = useState(true);
7072
const { arePermissionsApproved } = usePermissions();
7173
const [overlayType, setOverlayType] = useState<OverlayType>(OverlayType.None);
@@ -81,10 +83,6 @@ const MultipleMapsScreen = () => {
8183
const { navigationController, addListeners, removeListeners } =
8284
useNavigation();
8385

84-
useEffect(() => {
85-
console.log('mapViewController1 changed', mapViewController1);
86-
}, [mapViewController1]);
87-
8886
const onArrival = useCallback(
8987
(event: ArrivalEvent) => {
9088
if (event.isFinalDestination) {
@@ -303,7 +301,7 @@ const MultipleMapsScreen = () => {
303301
}, []);
304302

305303
return arePermissionsApproved ? (
306-
<View style={CommonStyles.container}>
304+
<View style={[CommonStyles.container, { paddingBottom: insets.bottom }]}>
307305
<View style={CommonStyles.buttonContainer}>
308306
<Button
309307
title={mapsVisible ? 'Hide maps' : 'Show maps'}

example/src/screens/NavigationScreen.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import React, { useEffect, useState, useMemo, useCallback } from 'react';
1818
import { Button, View } from 'react-native';
1919
import Snackbar from 'react-native-snackbar';
20+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2021

2122
import {
2223
NavigationInitErrorCode,
@@ -61,6 +62,7 @@ enum OverlayType {
6162

6263
const NavigationScreen = () => {
6364
const { arePermissionsApproved } = usePermissions();
65+
const insets = useSafeAreaInsets();
6466
const [overlayType, setOverlayType] = useState<OverlayType>(OverlayType.None);
6567
const [mapViewController, setMapViewController] =
6668
useState<MapViewController | null>(null);
@@ -294,7 +296,7 @@ const NavigationScreen = () => {
294296
};
295297

296298
return arePermissionsApproved ? (
297-
<View style={CommonStyles.container}>
299+
<View style={[CommonStyles.container, { paddingBottom: insets.bottom }]}>
298300
<NavigationView
299301
style={MapStyles.mapView}
300302
androidStylingOptions={MapStylingOptions.android}

ios/react-native-navigation-sdk/BaseCarSceneDelegate.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ - (void)sceneDidBecomeActive:(UIScene *)scene {
6969
- (void)attachSession {
7070
if ([NavModule sharedInstance] != nil && [[NavModule sharedInstance] hasSession] &&
7171
!_sessionAttached) {
72-
[self.navViewController attachToNavigationSession:[[NavModule sharedInstance] getSession]];
72+
[self.navViewController attachToNavigationSessionIfNeeded];
7373
[self.navViewController setHeaderEnabled:NO];
7474
[self.navViewController setRecenterButtonEnabled:NO];
7575
[self.navViewController setFooterEnabled:NO];

ios/react-native-navigation-sdk/NavModule.m

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ - (void)initializeSession {
110110
[self->_session.roadSnappedLocationProvider addListener:self];
111111

112112
NavViewModule *navViewModule = [NavViewModule sharedInstance];
113-
[navViewModule attachViewsToNavigationSession:_session];
113+
[navViewModule attachViewsToNavigationSession];
114114

115115
[self onNavigationReady];
116116
}
@@ -175,6 +175,7 @@ - (void)showTermsAndConditionsDialog {
175175
}
176176

177177
if (self->_session.navigator != nil) {
178+
[self->_session.navigator removeListener:self];
178179
[self->_session.navigator clearDestinations];
179180
self->_session.navigator.guidanceActive = NO;
180181
self->_session.navigator.sendsBackgroundNotifications = NO;
@@ -186,6 +187,10 @@ - (void)showTermsAndConditionsDialog {
186187

187188
self->_session.started = NO;
188189
self->_session = nil;
190+
191+
NavViewModule *navViewModule = [NavViewModule sharedInstance];
192+
[navViewModule navigationSessionDestroyed];
193+
189194
if (_navigationSessionDisposedCallback) {
190195
_navigationSessionDisposedCallback();
191196
}

ios/react-native-navigation-sdk/NavView.m

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,24 @@ - (void)handleGroundOverlayClick:(GMSGroundOverlay *)groundOverlay {
151151

152152
- (void)willMoveToSuperview:(UIView *)newSuperview {
153153
[super willMoveToSuperview:newSuperview];
154-
if (newSuperview == nil && _viewController && self.cleanupBlock) {
155-
// As newSuperview is nil, the view is being removed from its superview,
156-
// call the cleanup block provided by the view manager
154+
if (newSuperview == nil && _viewController) {
155+
// View is being removed from hierarchy, cleanup the view controller
156+
[self cleanup];
157+
}
158+
}
159+
160+
- (void)dealloc {
161+
[self cleanup];
162+
}
163+
164+
- (void)cleanup {
165+
if (self.cleanupBlock) {
157166
self.cleanupBlock(self.reactTag);
167+
self.cleanupBlock = nil;
168+
}
169+
170+
if (_viewController) {
171+
[_viewController.view removeFromSuperview];
158172
_viewController = nil;
159173
}
160174
}

ios/react-native-navigation-sdk/NavViewController.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ typedef void (^OnArrayResult)(NSArray *_Nullable result);
8181
- (void)removePolygon:(NSString *)polygonId;
8282
- (void)removeCircle:(NSString *)circleId;
8383
- (void)removeGroundOverlay:(NSString *)overlayId;
84-
- (BOOL)attachToNavigationSession:(GMSNavigationSession *)session;
84+
- (BOOL)attachToNavigationSessionIfNeeded;
85+
- (void)navigationSessionDestroyed;
8586
- (void)onPromptVisibilityChange:(BOOL)visible;
8687
- (void)setTravelMode:(GMSNavigationTravelMode)travelMode;
8788
- (void)setPadding:(UIEdgeInsets)insets;

ios/react-native-navigation-sdk/NavViewController.m

Lines changed: 118 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ @implementation NavViewController {
3636
NSString *_mapId;
3737
MapViewType _mapViewType;
3838
id<INavigationViewCallback> _viewCallbacks;
39+
BOOL _isSessionAttached;
40+
NSNumber *_isNavigationUIEnabled;
3941
}
4042

4143
- (instancetype)initWithMapViewType:(MapViewType)mapViewType {
@@ -62,17 +64,126 @@ - (void)loadView {
6264
}
6365

6466
_mapView = [[GMSMapView alloc] initWithOptions:options];
65-
67+
if (_mapViewType == NAVIGATION) {
68+
_mapView.navigationEnabled = YES;
69+
}
6670
self.view = _mapView;
6771
_mapView.delegate = self;
72+
}
73+
74+
- (void)viewDidLoad {
75+
[super viewDidLoad];
76+
77+
[_viewCallbacks handleMapReady];
78+
}
79+
80+
- (void)viewDidLayoutSubviews {
81+
[super viewDidLayoutSubviews];
82+
83+
[self attachToNavigationSessionIfNeeded];
84+
}
85+
86+
- (BOOL)attachToNavigationSessionIfNeeded {
87+
// Only attach if view has proper type, state and dimensions (not zero size)
88+
if (_mapViewType != NAVIGATION || _isSessionAttached || _mapView.bounds.size.width == 0 ||
89+
_mapView.bounds.size.height == 0) {
90+
return NO;
91+
}
6892

6993
NavModule *navModule = [NavModule sharedInstance];
70-
if (navModule != nil && [navModule hasSession]) {
71-
[self attachToNavigationSession:[navModule getSession]];
94+
if (navModule == nil || ![navModule hasSession]) {
95+
return NO;
7296
}
73-
dispatch_async(dispatch_get_main_queue(), ^{
74-
[_viewCallbacks handleMapReady];
75-
});
97+
98+
GMSNavigationSession *session = [navModule getSession];
99+
if (_mapView == nil || session == nil) {
100+
return NO;
101+
}
102+
103+
// `enableNavigationWithSession` returns false if TOS is not accepted.
104+
// This should not be possible in normal usage as the NavModule ensures TOS acceptance before
105+
// navigation session creation.
106+
BOOL result = [_mapView enableNavigationWithSession:session];
107+
108+
if (result) {
109+
_mapView.navigationUIDelegate = self;
110+
[self applyStylingOptions];
111+
112+
[self restoreNavigationUIState];
113+
114+
_isSessionAttached = YES;
115+
116+
[self forceInvalidateView];
117+
}
118+
119+
return result;
120+
}
121+
122+
- (void)forceInvalidateView {
123+
if (_mapView) {
124+
// Defer to next run loop to ensure view is properly sized
125+
dispatch_async(dispatch_get_main_queue(), ^{
126+
if (self->_mapView) {
127+
[self->_mapView setNeedsLayout];
128+
[self->_mapView.layer setNeedsDisplay];
129+
}
130+
});
131+
}
132+
}
133+
134+
- (void)restoreNavigationUIState {
135+
if (_mapView) {
136+
if (_isNavigationUIEnabled != nil) {
137+
_mapView.navigationEnabled = [_isNavigationUIEnabled boolValue];
138+
} else {
139+
_mapView.navigationEnabled = _mapViewType == NAVIGATION;
140+
}
141+
}
142+
}
143+
144+
- (void)navigationSessionDestroyed {
145+
_isSessionAttached = NO;
146+
if (_mapView) {
147+
_mapView.navigationUIDelegate = nil;
148+
_mapView.navigationEnabled = NO;
149+
}
150+
}
151+
152+
- (void)cleanup {
153+
_isSessionAttached = NO;
154+
155+
// Remove all delegates to break retain cycles
156+
if (_mapView) {
157+
_mapView.delegate = nil;
158+
_mapView.navigationUIDelegate = nil;
159+
_mapView.navigationEnabled = NO;
160+
[_mapView clear];
161+
162+
[_mapView removeFromSuperview];
163+
_mapView = nil;
164+
}
165+
166+
// Clear local arrays and set to nil
167+
[_markerList removeAllObjects];
168+
[_polylineList removeAllObjects];
169+
[_polygonList removeAllObjects];
170+
[_circleList removeAllObjects];
171+
[_groundOverlayList removeAllObjects];
172+
173+
_markerList = nil;
174+
_polylineList = nil;
175+
_polygonList = nil;
176+
_circleList = nil;
177+
_groundOverlayList = nil;
178+
179+
// Clear callbacks
180+
_viewCallbacks = nil;
181+
}
182+
183+
- (void)dealloc {
184+
[self cleanup];
185+
[self.view removeFromSuperview];
186+
self.view = nil;
76187
}
77188

78189
- (void)mapViewDidTapRecenterButton:(GMSMapView *)mapView {
@@ -195,6 +306,7 @@ - (void)setNavigationUIEnabled:(BOOL)isEnabled {
195306
if (_mapViewType != NAVIGATION) {
196307
return;
197308
}
309+
_isNavigationUIEnabled = @(isEnabled);
198310
_mapView.navigationEnabled = isEnabled;
199311
}
200312

@@ -327,16 +439,6 @@ - (void)setSpeedLimitIconEnabled:(BOOL)isEnabled {
327439

328440
#pragma mark - View Controller functions
329441

330-
- (BOOL)attachToNavigationSession:(GMSNavigationSession *)session {
331-
if (_mapViewType != NAVIGATION) {
332-
return NO;
333-
}
334-
BOOL result = [_mapView enableNavigationWithSession:session];
335-
_mapView.navigationUIDelegate = self;
336-
[self applyStylingOptions];
337-
return result;
338-
}
339-
340442
- (void)onPromptVisibilityChange:(BOOL)isVisible {
341443
[_viewCallbacks handlePromptVisibilityChanged:isVisible];
342444
}

0 commit comments

Comments
 (0)