Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[google_maps_flutter] Fix iOS crash by observing map frame change only once #3426

Merged
merged 19 commits into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ target 'Runner' do
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths

pod 'OCMock', '~> 3.9.1'
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -188,6 +191,8 @@
isa = PBXGroup;
children = (
F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */,
982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */,
982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */,
F7151F14265D7ED70028CB91 /* Info.plist */,
);
path = RunnerTests;
Expand Down Expand Up @@ -270,7 +275,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1100;
LastUpgradeCheck = 1320;
ORGANIZATIONNAME = "The Flutter Authors";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
Expand Down Expand Up @@ -441,6 +446,7 @@
buildActionMask = 2147483647;
files = (
F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */,
982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1100"
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -15,4 +19,24 @@ - (void)testPlugin {
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ @implementation FLTGoogleMapController {
FlutterMethodChannel *_channel;
BOOL _trackCameraPosition;
NSObject<FlutterPluginRegistrar> *_registrar;
BOOL _cameraDidInitialSetup;
FLTMarkersController *_markersController;
FLTPolygonsController *_polygonsController;
FLTPolylinesController *_polylinesController;
Expand All @@ -63,11 +62,19 @@ - (instancetype)initWithFrame:(CGRect)frame
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);
Expand All @@ -83,7 +90,6 @@ - (instancetype)initWithFrame:(CGRect)frame
}];
_mapView.delegate = weakSelf;
_registrar = registrar;
_cameraDidInitialSetup = NO;
_markersController = [[FLTMarkersController alloc] init:_channel
mapView:_mapView
registrar:registrar];
Expand Down Expand Up @@ -119,24 +125,20 @@ - (instancetype)initWithFrame:(CGRect)frame
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;
}

- (void)observeValueForKeyPath:(NSString *)keyPath
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)) {
Expand All @@ -146,7 +148,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath
// zero.
return;
}
_cameraDidInitialSetup = YES;
// We only observe the frame for initial setup.
[_mapView removeObserver:self forKeyPath:@"frame"];
[_mapView moveCamera:[GMSCameraUpdate setCamera:_mapView.camera]];
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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[];
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ Downloaded by pub (not CocoaPods).
s.author = { 'Flutter Dev Team' => '[email protected]' }
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: google_maps_flutter
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"
Expand Down