[google_maps_flutter] Fix iOS crash by observing map frame change only once (#3426)
diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
index 565bf84..f05de47 100644
--- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
+++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.1.3
+
+* Fixes iOS crash on `EXC_BAD_ACCESS KERN_PROTECTION_FAILURE` if the map frame changes long after creation.
+
## 2.1.2
* Removes dependencies from `pubspec.yaml` that are only needed in `example/pubspec.yaml`
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile
index 9686afa..29bfe63 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile
+++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile
@@ -31,6 +31,8 @@
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
+
+ pod 'OCMock', '~> 3.9.1'
end
end
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj
index fbb006a..6a0466c 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj
@@ -15,6 +15,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */; };
F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */; };
F7151F21265D7EE50028CB91 /* GoogleMapsUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F20265D7EE50028CB91 /* GoogleMapsUITests.m */; };
FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */; };
@@ -67,6 +68,8 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PartiallyMockedMapView.h; sourceTree = "<group>"; };
+ 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PartiallyMockedMapView.m; sourceTree = "<group>"; };
B7AFC65E3DD5AC60D834D83D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
@@ -188,6 +191,8 @@
isa = PBXGroup;
children = (
F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */,
+ 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */,
+ 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */,
F7151F14265D7ED70028CB91 /* Info.plist */,
);
path = RunnerTests;
@@ -270,7 +275,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1100;
+ LastUpgradeCheck = 1320;
ORGANIZATIONNAME = "The Flutter Authors";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -441,6 +446,7 @@
buildActionMask = 2147483647;
files = (
F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */,
+ 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -511,6 +517,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -567,6 +574,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index afdb55f..c983bfc 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
- LastUpgradeVersion = "1100"
+ LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/GoogleMapsTests.m b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/GoogleMapsTests.m
index a833c74..f03dca2 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/GoogleMapsTests.m
+++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/GoogleMapsTests.m
@@ -3,8 +3,12 @@
// found in the LICENSE file.
@import google_maps_flutter;
+@import google_maps_flutter.Test;
@import XCTest;
+#import <OCMock/OCMock.h>
+#import "PartiallyMockedMapView.h"
+
@interface GoogleMapsTests : XCTestCase
@end
@@ -15,4 +19,24 @@
XCTAssertNotNil(plugin);
}
+- (void)testFrameObserver {
+ id registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar));
+ CGRect frame = CGRectMake(0, 0, 100, 100);
+ PartiallyMockedMapView *mapView = [[PartiallyMockedMapView alloc]
+ initWithFrame:frame
+ camera:[[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0]];
+ FLTGoogleMapController *controller = [[FLTGoogleMapController alloc] initWithMapView:mapView
+ viewIdentifier:0
+ arguments:nil
+ registrar:registrar];
+
+ for (NSInteger i = 0; i < 10; ++i) {
+ [controller view];
+ }
+ XCTAssertEqual(mapView.frameObserverCount, 1);
+
+ mapView.frame = frame;
+ XCTAssertEqual(mapView.frameObserverCount, 0);
+}
+
@end
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.h b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.h
new file mode 100644
index 0000000..4288401
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.h
@@ -0,0 +1,17 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@import GoogleMaps;
+
+/**
+ * Defines a map view used for testing key-value observing.
+ */
+@interface PartiallyMockedMapView : GMSMapView
+
+/**
+ * The number of times that the `frame` KVO has been added.
+ */
+@property(nonatomic, assign, readonly) NSInteger frameObserverCount;
+
+@end
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.m b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.m
new file mode 100644
index 0000000..202a18d
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.m
@@ -0,0 +1,34 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "PartiallyMockedMapView.h"
+
+@interface PartiallyMockedMapView ()
+
+@property(nonatomic, assign) NSInteger frameObserverCount;
+
+@end
+
+@implementation PartiallyMockedMapView
+
+- (void)addObserver:(NSObject *)observer
+ forKeyPath:(NSString *)keyPath
+ options:(NSKeyValueObservingOptions)options
+ context:(void *)context {
+ [super addObserver:observer forKeyPath:keyPath options:options context:context];
+
+ if ([keyPath isEqualToString:@"frame"]) {
+ ++self.frameObserverCount;
+ }
+}
+
+- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
+ [super removeObserver:observer forKeyPath:keyPath];
+
+ if ([keyPath isEqualToString:@"frame"]) {
+ --self.frameObserverCount;
+ }
+}
+
+@end
diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m
index df4e876..ca80681 100644
--- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m
+++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m
@@ -51,7 +51,6 @@
FlutterMethodChannel *_channel;
BOOL _trackCameraPosition;
NSObject<FlutterPluginRegistrar> *_registrar;
- BOOL _cameraDidInitialSetup;
FLTMarkersController *_markersController;
FLTPolygonsController *_polygonsController;
FLTPolylinesController *_polylinesController;
@@ -63,11 +62,19 @@
viewIdentifier:(int64_t)viewId
arguments:(id _Nullable)args
registrar:(NSObject<FlutterPluginRegistrar> *)registrar {
+ GMSCameraPosition *camera = ToOptionalCameraPosition(args[@"initialCameraPosition"]);
+ GMSMapView *mapView = [GMSMapView mapWithFrame:frame camera:camera];
+ return [self initWithMapView:mapView viewIdentifier:viewId arguments:args registrar:registrar];
+}
+
+- (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args
+ registrar:(NSObject<FlutterPluginRegistrar> *_Nonnull)registrar {
if (self = [super init]) {
+ _mapView = mapView;
_viewId = viewId;
- GMSCameraPosition *camera = ToOptionalCameraPosition(args[@"initialCameraPosition"]);
- _mapView = [GMSMapView mapWithFrame:frame camera:camera];
_mapView.accessibilityElementsHidden = NO;
_trackCameraPosition = NO;
InterpretMapOptions(args[@"options"], self);
@@ -83,7 +90,6 @@
}];
_mapView.delegate = weakSelf;
_registrar = registrar;
- _cameraDidInitialSetup = NO;
_markersController = [[FLTMarkersController alloc] init:_channel
mapView:_mapView
registrar:registrar];
@@ -119,12 +125,13 @@
if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) {
[_tileOverlaysController addTileOverlays:tileOverlaysToAdd];
}
+
+ [_mapView addObserver:self forKeyPath:@"frame" options:0 context:nil];
}
return self;
}
- (UIView *)view {
- [_mapView addObserver:self forKeyPath:@"frame" options:0 context:nil];
return _mapView;
}
@@ -132,11 +139,6 @@
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
- if (_cameraDidInitialSetup) {
- // We only observe the frame for initial setup.
- [_mapView removeObserver:self forKeyPath:@"frame"];
- return;
- }
if (object == _mapView && [keyPath isEqualToString:@"frame"]) {
CGRect bounds = _mapView.bounds;
if (CGRectEqualToRect(bounds, CGRectZero)) {
@@ -146,7 +148,7 @@
// zero.
return;
}
- _cameraDidInitialSetup = YES;
+ // We only observe the frame for initial setup.
[_mapView removeObserver:self forKeyPath:@"frame"];
[_mapView moveCamera:[GMSCameraUpdate setCamera:_mapView.camera]];
} else {
diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController_Test.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController_Test.h
new file mode 100644
index 0000000..84f6f7c
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController_Test.h
@@ -0,0 +1,27 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Flutter/Flutter.h>
+#import <GoogleMaps/GoogleMaps.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FLTGoogleMapController (Test)
+
+/**
+ * Initializes a map controller with a concrete map view.
+ *
+ * @param mapView A map view that will be displayed by the controller
+ * @param viewId A unique identifier for the controller.
+ * @param args Parameters for initialising the map view.
+ * @param registrar The plugin registrar passed from Flutter.
+ */
+- (instancetype)initWithMapView:(GMSMapView *)mapView
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args
+ registrar:(NSObject<FlutterPluginRegistrar> *)registrar;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h
new file mode 100644
index 0000000..50880a2
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h
@@ -0,0 +1,11 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Foundation/Foundation.h>
+#import <google_maps_flutter/FLTGoogleMapTileOverlayController.h>
+#import <google_maps_flutter/FLTGoogleMapsPlugin.h>
+#import <google_maps_flutter/JsonConversions.h>
+
+FOUNDATION_EXPORT double google_maps_flutterVersionNumber;
+FOUNDATION_EXPORT const unsigned char google_maps_flutterVersionString[];
diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter.modulemap b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter.modulemap
new file mode 100644
index 0000000..19513f4
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter.modulemap
@@ -0,0 +1,10 @@
+framework module google_maps_flutter {
+ umbrella header "google_maps_flutter-umbrella.h"
+
+ export *
+ module * { export * }
+
+ explicit module Test {
+ header "GoogleMapController_Test.h"
+ }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec b/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec
index f2ed5fc..e34919c 100644
--- a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec
+++ b/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec
@@ -14,8 +14,9 @@
s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter' }
s.documentation_url = 'https://pub.dev/packages/google_maps_flutter'
- s.source_files = 'Classes/**/*'
+ s.source_files = 'Classes/**/*.{h,m}'
s.public_header_files = 'Classes/**/*.h'
+ s.module_map = 'Classes/google_maps_flutter.modulemap'
s.dependency 'Flutter'
s.dependency 'GoogleMaps'
s.static_framework = true
diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
index 849019c..741fe69 100644
--- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
@@ -2,7 +2,7 @@
description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
-version: 2.1.2
+version: 2.1.3
environment:
sdk: ">=2.14.0 <3.0.0"