Skip to content

Commit 64a2d8e

Browse files
committed
feat(ios)!: integrate native dependencies via Swift Package Manager
GoogleMaps, GoogleMapsUtils and SVGKit are now resolved via SPM through react-native's spm_dependency helper instead of CocoaPods. The API key is read from Info.plist by the library itself and a missing key is reported via onMapError. BREAKING CHANGE: Requires react-native >= 0.87.0. iOS apps must remove the svgkit patch from their Podfile (`require_relative '.../scripts/svgkit_patch'` and `apply_svgkit_patch(installer)`), install pods as frameworks (`use_frameworks!`, either `:dynamic` or `:static`) and remove `GMSServices.provideAPIKey` from AppDelegate, otherwise every map reports a spurious onMapError.
1 parent 47db20e commit 64a2d8e

29 files changed

Lines changed: 1404 additions & 1610 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![Dev Release](https://img.shields.io/npm/v/react-native-google-maps-plus/dev.svg?label=dev%20release&color=orange)](https://www.npmjs.com/package/react-native-google-maps-plus)
55
[![Build](https://github.com/pinpong/react-native-google-maps-plus/actions/workflows/release.yml/badge.svg)](https://github.com/pinpong/react-native-google-maps-plus/actions/workflows/release.yml)
66
[![API Docs](https://img.shields.io/static/v1?label=typedoc&message=docs&color=informational)](https://pinpong.github.io/react-native-google-maps-plus)
7-
![React Native](https://img.shields.io/badge/react--native-%3E%3D0.82.0-61dafb.svg?logo=react)
7+
![React Native](https://img.shields.io/badge/react--native-%3E%3D0.87.0-61dafb.svg?logo=react)
88

99
React Native wrapper for Android & iOS Google Maps SDK with Street View support
1010

RNGoogleMapsPlus.podspec

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,21 @@ Pod::Spec.new do |s|
2424
s.dependency 'React-jsi'
2525
s.dependency 'React-callinvoker'
2626

27-
s.dependency 'GoogleMaps', '10.15.0'
28-
s.dependency 'Google-Maps-iOS-Utils', '7.0.0'
29-
s.dependency 'SVGKit', '3.0.0'
27+
spm_dependency(s,
28+
url: 'https://github.com/googlemaps/ios-maps-sdk',
29+
requirement: { kind: 'exactVersion', version: '10.15.0' },
30+
products: ['GoogleMaps']
31+
)
32+
spm_dependency(s,
33+
url: 'https://github.com/googlemaps/google-maps-ios-utils',
34+
requirement: { kind: 'exactVersion', version: '7.0.0' },
35+
products: ['GoogleMapsUtils']
36+
)
37+
spm_dependency(s,
38+
url: 'https://github.com/SVGKit/SVGKit',
39+
requirement: { kind: 'exactVersion', version: '3.0.0' },
40+
products: ['SVGKit']
41+
)
3042

3143
load 'nitrogen/generated/ios/RNGoogleMapsPlus+autolinking.rb'
3244
add_nitrogen_files(s)

__tests__/expoConfig/__snapshots__/withIosGoogleMapsPlus.test.ts.snap

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,5 @@
11
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

3-
exports[`withIosGoogleMapsPlus AppDelegate matches the snapshot for the current Expo template 1`] = `
4-
"internal import Expo
5-
import React
6-
// @generated begin react-native-google-maps-import - expo prebuild (DO NOT MODIFY) sync-3b9e722debd3c073b9705963208f8121ec9f576c
7-
import GoogleMaps
8-
// @generated end react-native-google-maps-import
9-
import ReactAppDependencyProvider
10-
11-
@main
12-
class AppDelegate: ExpoAppDelegate {
13-
var window: UIWindow?
14-
15-
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
16-
var reactNativeFactory: RCTReactNativeFactory?
17-
18-
public override func application(
19-
_ application: UIApplication,
20-
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
21-
) -> Bool {
22-
let delegate = ReactNativeDelegate()
23-
let factory = ExpoReactNativeFactory(delegate: delegate)
24-
delegate.dependencyProvider = RCTAppDependencyProvider()
25-
26-
reactNativeDelegate = delegate
27-
reactNativeFactory = factory
28-
29-
#if os(iOS) || os(tvOS)
30-
window = UIWindow(frame: UIScreen.main.bounds)
31-
factory.startReactNative(
32-
withModuleName: "main",
33-
in: window,
34-
launchOptions: launchOptions)
35-
#endif
36-
// @generated begin react-native-google-maps-init - expo prebuild (DO NOT MODIFY) sync-ed7def55dfd1b52fb685aa33a269aeb34fc6ab13
37-
38-
if let apiKey = Bundle.main.object(forInfoDictionaryKey: "MAPS_API_KEY") as? String {
39-
GMSServices.provideAPIKey(apiKey)
40-
}
41-
// @generated end react-native-google-maps-init
42-
43-
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
44-
}
45-
46-
// Linking API
47-
public override func application(
48-
_ app: UIApplication,
49-
open url: URL,
50-
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
51-
) -> Bool {
52-
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
53-
}
54-
55-
// Universal Links
56-
public override func application(
57-
_ application: UIApplication,
58-
continue userActivity: NSUserActivity,
59-
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
60-
) -> Bool {
61-
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
62-
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
63-
}
64-
}
65-
66-
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
67-
// Extension point for config-plugins
68-
69-
override func sourceURL(for bridge: RCTBridge) -> URL? {
70-
// needed to return the correct URL for expo-dev-client.
71-
bridge.bundleURL ?? bundleURL()
72-
}
73-
74-
override func bundleURL() -> URL? {
75-
#if DEBUG
76-
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
77-
#else
78-
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
79-
#endif
80-
}
81-
}
82-
"
83-
`;
84-
853
exports[`withIosGoogleMapsPlus Podfile matches the snapshot for the current Expo template 1`] = `
864
"require File.join(File.dirname(\`node --print "require.resolve('expo/package.json')"\`), "scripts/autolinking")
875
require File.join(File.dirname(\`node --print "require.resolve('react-native/package.json')"\`), "scripts/react_native_pods")
@@ -145,10 +63,6 @@ target 'HelloWorld' do
14563
)
14664
14765
post_install do |installer|
148-
# @generated begin react-native-google-maps-svgkit-patch - expo prebuild (DO NOT MODIFY) sync-ed16ec228929e9f6bcce5f8654ab58fb1bdadeea
149-
require File.join(File.dirname(\`node --print "require.resolve('react-native-google-maps-plus/package.json')"\`), 'scripts', 'svgkit_patch')
150-
apply_svgkit_patch(installer)
151-
# @generated end react-native-google-maps-svgkit-patch
15266
react_native_post_install(
15367
installer,
15468
config[:reactNativePath],

__tests__/expoConfig/withIosGoogleMapsPlus.test.ts

Lines changed: 51 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,23 @@ const PROPS: RNGoogleMapsPlusExpoPluginProps = {
3030
googleMapsIosApiKey: 'ios-key',
3131
};
3232

33-
const LEGACY_APP_DELEGATE = `import UIKit
34-
import React
33+
const LEGACY_PODFILE_BLOCK = `# @generated begin react-native-google-maps-svgkit-patch - expo prebuild (DO NOT MODIFY) sync-ed16ec228929e9f6bcce5f8654ab58fb1bdadeea
34+
require File.join(File.dirname(\`node --print "require.resolve('react-native-google-maps-plus/package.json')"\`), 'scripts', 'svgkit_patch')
35+
apply_svgkit_patch(installer)
36+
# @generated end react-native-google-maps-svgkit-patch
37+
`;
38+
39+
const LEGACY_APP_DELEGATE_IMPORT = `// @generated begin react-native-google-maps-import - expo prebuild (DO NOT MODIFY) sync-3b9e722debd3c073b9705963208f8121ec9f576c
40+
import GoogleMaps
41+
// @generated end react-native-google-maps-import
42+
`;
3543

36-
@UIApplicationMain
37-
class AppDelegate: UIResponder, UIApplicationDelegate {
38-
var window: UIWindow?
44+
const LEGACY_APP_DELEGATE_INIT = `// @generated begin react-native-google-maps-init - expo prebuild (DO NOT MODIFY) sync-ed7def55dfd1b52fb685aa33a269aeb34fc6ab13
3945
40-
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
41-
return true
42-
}
43-
}
46+
if let apiKey = Bundle.main.object(forInfoDictionaryKey: "MAPS_API_KEY") as? String {
47+
GMSServices.provideAPIKey(apiKey)
48+
}
49+
// @generated end react-native-google-maps-init
4450
`;
4551

4652
type PluginMods = {
@@ -54,7 +60,6 @@ type ApplyOptions = {
5460
infoPlist?: Record<string, unknown>;
5561
podfile?: string;
5662
appDelegate?: string;
57-
appDelegateLanguage?: string;
5863
};
5964

6065
function readFixture(name: string): string {
@@ -66,7 +71,6 @@ function applyPlugin({
6671
infoPlist = {},
6772
podfile = readFixture('Podfile'),
6873
appDelegate = readFixture('AppDelegate.swift'),
69-
appDelegateLanguage = 'swift',
7074
}: ApplyOptions = {}): PluginMods {
7175
const config = {
7276
name: 'example',
@@ -76,7 +80,7 @@ function applyPlugin({
7680
podfile: { path: 'ios/Podfile', language: 'rb', contents: podfile },
7781
appDelegate: {
7882
path: 'ios/AppDelegate.swift',
79-
language: appDelegateLanguage,
83+
language: 'swift',
8084
contents: appDelegate,
8185
},
8286
},
@@ -94,7 +98,6 @@ function reapply(mods: PluginMods, props?: ApplyOptions['props']): PluginMods {
9498
infoPlist: mods.infoPlist,
9599
podfile: mods.podfile.contents,
96100
appDelegate: mods.appDelegate.contents,
97-
appDelegateLanguage: mods.appDelegate.language,
98101
});
99102
}
100103

@@ -147,76 +150,28 @@ describe('withIosGoogleMapsPlus', () => {
147150
);
148151
});
149152

150-
it('injects the svgkit patch into the existing post_install block', () => {
151-
const { contents } = applyPlugin({ props: PROPS }).podfile;
152-
153-
expect(contents).toContain(
154-
`require File.join(File.dirname(\`node --print "require.resolve('react-native-google-maps-plus/package.json')"\`), 'scripts', 'svgkit_patch')`
155-
);
156-
expect(count(contents, 'post_install do |installer|')).toBe(1);
157-
expect(contents.indexOf('apply_svgkit_patch(installer)')).toBeGreaterThan(
158-
contents.indexOf('post_install do |installer|')
159-
);
160-
});
161-
162-
it('places the patch outside a nested if/end block from another plugin', () => {
163-
const nestedEndPodfile = `platform :ios, '16.0'
164-
165-
target 'Example' do
166-
post_install do |installer|
167-
if ENV['CI'] == 'true'
168-
puts 'ci run'
169-
end
170-
react_native_post_install(installer)
171-
end
172-
end
173-
`;
174-
const { contents } = applyPlugin({
175-
props: PROPS,
176-
podfile: nestedEndPodfile,
177-
}).podfile;
178-
179-
expect(count(contents, 'apply_svgkit_patch(installer)')).toBe(1);
180-
expect(contents.indexOf('apply_svgkit_patch(installer)')).toBeLessThan(
181-
contents.indexOf("if ENV['CI']")
182-
);
183-
});
184-
185-
it('appends a post_install block when the Podfile has none', () => {
186-
const minimalPodfile = `platform :ios, '16.0'
187-
188-
target 'Example' do
189-
end
190-
`;
191-
const { contents } = applyPlugin({
192-
props: PROPS,
193-
podfile: minimalPodfile,
194-
}).podfile;
195-
196-
expect(count(contents, 'post_install do |installer|')).toBe(1);
197-
expect(contents).toContain('apply_svgkit_patch(installer)');
198-
});
199-
200153
it('is idempotent when applied twice', () => {
201154
const first = applyPlugin({ props: PROPS });
202155
const second = reapply(first, PROPS);
203156

204157
expect(second.podfile.contents).toBe(first.podfile.contents);
205158
});
206159

207-
it('stays idempotent when the first run had to append the post_install block', () => {
208-
const minimalPodfile = `platform :ios, '16.0'
160+
it('removes the svgkit patch block generated by previous versions', () => {
161+
const legacy = readFixture('Podfile').replace(
162+
'post_install do |installer|\n',
163+
`post_install do |installer|\n${LEGACY_PODFILE_BLOCK}`
164+
);
165+
expect(legacy).toContain('apply_svgkit_patch(installer)');
209166

210-
target 'Example' do
211-
end
212-
`;
213-
const first = applyPlugin({ props: PROPS, podfile: minimalPodfile });
214-
const second = reapply(first, PROPS);
167+
const { contents } = applyPlugin({
168+
props: PROPS,
169+
podfile: legacy,
170+
}).podfile;
215171

216-
expect(second.podfile.contents).toBe(first.podfile.contents);
217-
expect(
218-
count(first.podfile.contents, 'apply_svgkit_patch(installer)')
219-
).toBe(1);
172+
expect(contents).not.toContain('svgkit_patch');
173+
expect(contents).not.toContain('react-native-google-maps-svgkit-patch');
174+
expect(contents).toBe(applyPlugin({ props: PROPS }).podfile.contents);
220175
});
221176

222177
it('matches the snapshot for the current Expo template', () => {
@@ -226,66 +181,41 @@ end
226181
});
227182

228183
describe('AppDelegate', () => {
229-
it('adds the GoogleMaps import after import React', () => {
184+
it('leaves a clean AppDelegate untouched', () => {
185+
const fixture = readFixture('AppDelegate.swift');
230186
const { contents } = applyPlugin({ props: PROPS }).appDelegate;
231187

232-
expect(count(contents, 'import GoogleMaps')).toBe(1);
233-
expect(contents.indexOf('import GoogleMaps')).toBeGreaterThan(
234-
contents.indexOf('import React')
235-
);
188+
expect(contents).toBe(fixture);
236189
});
237190

238-
it('initializes GMSServices inside didFinishLaunchingWithOptions before the super call', () => {
239-
const { contents } = applyPlugin({ props: PROPS }).appDelegate;
240-
241-
const initIndex = contents.indexOf('GMSServices.provideAPIKey');
242-
expect(initIndex).toBeGreaterThan(
243-
contents.indexOf('didFinishLaunchingWithOptions')
244-
);
245-
expect(initIndex).toBeLessThan(
246-
contents.indexOf(
247-
'return super.application(application, didFinishLaunchingWithOptions: launchOptions)'
191+
it('removes the import and init blocks generated by previous versions', () => {
192+
const legacy = readFixture('AppDelegate.swift')
193+
.replace(
194+
'import React\n',
195+
`import React\n${LEGACY_APP_DELEGATE_IMPORT}`
248196
)
249-
);
250-
});
251-
252-
it('is idempotent when applied twice', () => {
253-
const first = applyPlugin({ props: PROPS });
254-
const second = reapply(first, PROPS);
255-
256-
expect(second.appDelegate.contents).toBe(first.appDelegate.contents);
257-
});
197+
.replace(
198+
/^(\s*return super\.application\()/m,
199+
`${LEGACY_APP_DELEGATE_INIT}$1`
200+
);
201+
expect(legacy).toContain('GMSServices.provideAPIKey');
258202

259-
it('falls back to the didFinishLaunchingWithOptions anchor when there is no super call', () => {
260203
const { contents } = applyPlugin({
261204
props: PROPS,
262-
appDelegate: LEGACY_APP_DELEGATE,
205+
appDelegate: legacy,
263206
}).appDelegate;
264207

265-
const initIndex = contents.indexOf('GMSServices.provideAPIKey');
266-
expect(initIndex).toBeGreaterThan(
267-
contents.indexOf('didFinishLaunchingWithOptions')
268-
);
269-
expect(initIndex).toBeLessThan(contents.indexOf('return true'));
208+
expect(contents).not.toContain('import GoogleMaps');
209+
expect(contents).not.toContain('GMSServices');
210+
expect(contents).not.toContain('@generated');
211+
expect(contents).toBe(readFixture('AppDelegate.swift'));
270212
});
271213

272-
it('skips injection and warns for a non-swift AppDelegate', () => {
273-
const objcAppDelegate = '#import "AppDelegate.h"\n';
274-
const { contents } = applyPlugin({
275-
props: PROPS,
276-
appDelegate: objcAppDelegate,
277-
appDelegateLanguage: 'objcpp',
278-
}).appDelegate;
279-
280-
expect(contents).toBe(objcAppDelegate);
281-
expect(console.warn).toHaveBeenCalledWith(
282-
expect.stringContaining('AppDelegate is not Swift')
283-
);
284-
});
214+
it('is idempotent when applied twice', () => {
215+
const first = applyPlugin({ props: PROPS });
216+
const second = reapply(first, PROPS);
285217

286-
it('matches the snapshot for the current Expo template', () => {
287-
const { contents } = applyPlugin({ props: PROPS }).appDelegate;
288-
expect(contents).toMatchSnapshot();
218+
expect(second.appDelegate.contents).toBe(first.appDelegate.contents);
289219
});
290220
});
291221
});

android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def getExtOrIntegerDefault(name) {
3737

3838
android {
3939
namespace "com.rngooglemapsplus"
40+
ndkVersion getExtOrDefault("ndkVersion")
4041

4142
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
4243

android/gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
RNGoogleMapsPlus_kotlinVersion=2.3.0
22
RNGoogleMapsPlus_minSdkVersion=26
3-
RNGoogleMapsPlus_targetSdkVersion=36
4-
RNGoogleMapsPlus_compileSdkVersion=36
3+
RNGoogleMapsPlus_targetSdkVersion=37
4+
RNGoogleMapsPlus_compileSdkVersion=37
55
RNGoogleMapsPlus_ndkVersion=27.1.12297006
66
RNGoogleMapsPlus_googlePlayServicesBaseVersion=18.10.0
77
RNGoogleMapsPlus_googlePlayServicesMapsVersion=20.0.0

0 commit comments

Comments
 (0)