Skip to content

Commit 1e7a00a

Browse files
[google_maps_flutter] Set properties before adding maps objects (#10347)
It appears that the Google Maps SDK on iOS renders on a different thread, and doesn't necessarily batch all updates that happen in a runloop the way standard iOS UI would, so that adding an object to the map before setting all of its properties can cause a flicker of the default property. This ensures that updating the native maps object from the Dart version sets all properties before adding the object to the map, and refactors to make unit testing the update helper simpler (and in one case, to fix an init-calls-self violation). Fixes flutter/flutter#143570 ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 7b153c9 commit 1e7a00a

28 files changed

+1143
-420
lines changed

packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
## NEXT
1+
## 2.15.6
22

3+
* Fixes potential flickers of default property values when adding objects to
4+
the map.
35
* Updates minimum supported SDK version to Flutter 3.32/Dart 3.8.
46

57
## 2.15.5

packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
2A6906C72D263DF4001F8426 /* GoogleMapsGroundOverlayControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A6906C62D263DE7001F8426 /* GoogleMapsGroundOverlayControllerTests.m */; };
1313
2BDE99378062AE3E60B40021 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3ACE0AFE8D82CD5962486AFD /* Pods_RunnerTests.framework */; };
1414
330909FF2D99B7A60077A751 /* GoogleMapsMarkerControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 330909FE2D99B79B0077A751 /* GoogleMapsMarkerControllerTests.m */; };
15+
339355BA2EB3E50300EBF864 /* GoogleMapsCircleControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 339355B92EB3E4F900EBF864 /* GoogleMapsCircleControllerTests.m */; };
16+
339355BD2EB3E56300EBF864 /* GoogleMapsTileOverlayControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 339355BC2EB3E55600EBF864 /* GoogleMapsTileOverlayControllerTests.m */; };
17+
339355BF2EB535A600EBF864 /* FLTGoogleMapHeatmapControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 339355BE2EB5359B00EBF864 /* FLTGoogleMapHeatmapControllerTests.m */; };
1518
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
16-
478116522BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 478116512BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m */; };
19+
478116522BEF8F47002F593E /* GoogleMapsPolylineControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 478116512BEF8F47002F593E /* GoogleMapsPolylineControllerTests.m */; };
1720
528F16832C62941000148160 /* FGMClusterManagersControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 528F16822C62941000148160 /* FGMClusterManagersControllerTests.m */; };
1821
528F16872C62952700148160 /* ExtractIconFromDataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 528F16862C62952700148160 /* ExtractIconFromDataTests.m */; };
1922
6851F3562835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */; };
@@ -66,14 +69,19 @@
6669
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
6770
2A6906C62D263DE7001F8426 /* GoogleMapsGroundOverlayControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsGroundOverlayControllerTests.m; sourceTree = "<group>"; };
6871
330909FE2D99B79B0077A751 /* GoogleMapsMarkerControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsMarkerControllerTests.m; sourceTree = "<group>"; };
72+
339355B82EB3E4D500EBF864 /* GoogleMapsPolygonControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsPolygonControllerTests.m; sourceTree = "<group>"; };
73+
339355B92EB3E4F900EBF864 /* GoogleMapsCircleControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsCircleControllerTests.m; sourceTree = "<group>"; };
74+
339355BC2EB3E55600EBF864 /* GoogleMapsTileOverlayControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsTileOverlayControllerTests.m; sourceTree = "<group>"; };
75+
339355BE2EB5359B00EBF864 /* FLTGoogleMapHeatmapControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTGoogleMapHeatmapControllerTests.m; sourceTree = "<group>"; };
6976
3ACE0AFE8D82CD5962486AFD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7077
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
71-
478116512BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsPolylinesControllerTests.m; sourceTree = "<group>"; };
78+
478116512BEF8F47002F593E /* GoogleMapsPolylineControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsPolylineControllerTests.m; sourceTree = "<group>"; };
7279
528F16822C62941000148160 /* FGMClusterManagersControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FGMClusterManagersControllerTests.m; sourceTree = "<group>"; };
7380
528F16862C62952700148160 /* ExtractIconFromDataTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExtractIconFromDataTests.m; sourceTree = "<group>"; };
7481
61A9A8623F5CA9BBC813DC6B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7582
6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTGoogleMapJSONConversionsConversionTests.m; sourceTree = "<group>"; };
7683
733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
84+
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7785
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7886
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7987
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
@@ -139,6 +147,7 @@
139147
9740EEB11CF90186004384FC /* Flutter */ = {
140148
isa = PBXGroup;
141149
children = (
150+
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
142151
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
143152
9740EEB21CF90195004384FC /* Debug.xcconfig */,
144153
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -211,12 +220,16 @@
211220
F269303A2BB389BF00BF17C4 /* assets */,
212221
528F16862C62952700148160 /* ExtractIconFromDataTests.m */,
213222
528F16822C62941000148160 /* FGMClusterManagersControllerTests.m */,
223+
339355BE2EB5359B00EBF864 /* FLTGoogleMapHeatmapControllerTests.m */,
214224
6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */,
215225
0DD7B6C22B744EEF00E857FD /* FLTTileProviderControllerTests.m */,
216226
F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */,
217-
330909FE2D99B79B0077A751 /* GoogleMapsMarkerControllerTests.m */,
218-
478116512BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m */,
227+
339355B92EB3E4F900EBF864 /* GoogleMapsCircleControllerTests.m */,
219228
2A6906C62D263DE7001F8426 /* GoogleMapsGroundOverlayControllerTests.m */,
229+
330909FE2D99B79B0077A751 /* GoogleMapsMarkerControllerTests.m */,
230+
339355B82EB3E4D500EBF864 /* GoogleMapsPolygonControllerTests.m */,
231+
478116512BEF8F47002F593E /* GoogleMapsPolylineControllerTests.m */,
232+
339355BC2EB3E55600EBF864 /* GoogleMapsTileOverlayControllerTests.m */,
220233
982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */,
221234
982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */,
222235
F7151F14265D7ED70028CB91 /* Info.plist */,
@@ -248,7 +261,6 @@
248261
9705A1C41CF9048500538489 /* Embed Frameworks */,
249262
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
250263
BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */,
251-
9C5FE6CAF02237D44998DDC0 /* [CP] Embed Pods Frameworks */,
252264
);
253265
buildRules = (
254266
);
@@ -426,24 +438,6 @@
426438
shellPath = /bin/sh;
427439
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
428440
};
429-
9C5FE6CAF02237D44998DDC0 /* [CP] Embed Pods Frameworks */ = {
430-
isa = PBXShellScriptBuildPhase;
431-
buildActionMask = 2147483647;
432-
files = (
433-
);
434-
inputPaths = (
435-
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
436-
"${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
437-
);
438-
name = "[CP] Embed Pods Frameworks";
439-
outputPaths = (
440-
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
441-
);
442-
runOnlyForDeploymentPostprocessing = 0;
443-
shellPath = /bin/sh;
444-
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
445-
showEnvVarsInLog = 0;
446-
};
447441
BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */ = {
448442
isa = PBXShellScriptBuildPhase;
449443
buildActionMask = 2147483647;
@@ -522,11 +516,14 @@
522516
buildActionMask = 2147483647;
523517
files = (
524518
528F16832C62941000148160 /* FGMClusterManagersControllerTests.m in Sources */,
519+
339355BA2EB3E50300EBF864 /* GoogleMapsCircleControllerTests.m in Sources */,
520+
339355BF2EB535A600EBF864 /* FLTGoogleMapHeatmapControllerTests.m in Sources */,
521+
339355BD2EB3E56300EBF864 /* GoogleMapsTileOverlayControllerTests.m in Sources */,
525522
F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */,
526523
6851F3562835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m in Sources */,
527524
982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */,
528525
330909FF2D99B7A60077A751 /* GoogleMapsMarkerControllerTests.m in Sources */,
529-
478116522BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m in Sources */,
526+
478116522BEF8F47002F593E /* GoogleMapsPolylineControllerTests.m in Sources */,
530527
2A6906C72D263DF4001F8426 /* GoogleMapsGroundOverlayControllerTests.m in Sources */,
531528
0DD7B6C32B744EEF00E857FD /* FLTTileProviderControllerTests.m in Sources */,
532529
528F16872C62952700148160 /* ExtractIconFromDataTests.m in Sources */,
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2013 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@import google_maps_flutter_ios;
6+
@import google_maps_flutter_ios.Test;
7+
@import XCTest;
8+
@import GoogleMaps;
9+
@import GoogleMapsUtils;
10+
11+
#import "PartiallyMockedMapView.h"
12+
13+
@interface PropertyOrderValidatingHeatmap : GMUHeatmapTileLayer {
14+
}
15+
@property(nonatomic) BOOL hasSetMap;
16+
@end
17+
18+
@interface GoogleMapsHeatmapControllerTests : XCTestCase
19+
@end
20+
21+
@implementation GoogleMapsHeatmapControllerTests
22+
23+
- (void)testUpdateHeatmapSetsVisibilityLast {
24+
PropertyOrderValidatingHeatmap *heatmap = [[PropertyOrderValidatingHeatmap alloc] init];
25+
[FLTGoogleMapHeatmapController
26+
updateHeatmap:heatmap
27+
fromOptions:@{
28+
@"data" : @[ @[ @[ @(5), @(5) ], @(0.5) ], @[ @[ @(10), @(10) ], @(0.75) ] ],
29+
@"gradient" : @{
30+
@"colors" : @[ @(0), @(1) ],
31+
@"startPoints" : @[ @(0), @(1) ],
32+
@"colorMapSize" : @(256),
33+
},
34+
@"opacity" : @(0.5),
35+
@"radius" : @(1),
36+
@"minimumZoomIntensity" : @(1),
37+
@"maximumZoomIntensity" : @(2),
38+
}
39+
withMapView:[GoogleMapsHeatmapControllerTests mapView]];
40+
XCTAssertTrue(heatmap.hasSetMap);
41+
}
42+
43+
/// Returns a simple map view to add map objects to.
44+
+ (GMSMapView *)mapView {
45+
GMSMapViewOptions *mapViewOptions = [[GMSMapViewOptions alloc] init];
46+
mapViewOptions.frame = CGRectMake(0, 0, 100, 100);
47+
mapViewOptions.camera = [[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0];
48+
return [[PartiallyMockedMapView alloc] initWithOptions:mapViewOptions];
49+
}
50+
51+
@end
52+
53+
@implementation PropertyOrderValidatingHeatmap
54+
55+
- (void)setWeightedData:(NSArray<GMUWeightedLatLng *> *)weightedData {
56+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
57+
super.weightedData = weightedData;
58+
}
59+
60+
- (void)setRadius:(NSUInteger)radius {
61+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
62+
super.radius = radius;
63+
}
64+
65+
- (void)setGradient:(GMUGradient *)gradient {
66+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
67+
super.gradient = gradient;
68+
}
69+
70+
- (void)setMinimumZoomIntensity:(NSUInteger)minimumZoomIntensity {
71+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
72+
super.minimumZoomIntensity = minimumZoomIntensity;
73+
}
74+
75+
- (void)setMaximumZoomIntensity:(NSUInteger)maximumZoomIntensity {
76+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
77+
super.maximumZoomIntensity = maximumZoomIntensity;
78+
}
79+
80+
- (void)setZIndex:(int)zIndex {
81+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
82+
super.zIndex = zIndex;
83+
}
84+
85+
- (void)setTileSize:(NSInteger)tileSize {
86+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
87+
super.tileSize = tileSize;
88+
}
89+
90+
- (void)setOpacity:(float)opacity {
91+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
92+
super.opacity = opacity;
93+
}
94+
95+
- (void)setFadeIn:(BOOL)fadeIn {
96+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
97+
super.fadeIn = fadeIn;
98+
}
99+
100+
- (void)setMap:(GMSMapView *)map {
101+
// Don't actually set the map, since that requires more test setup.
102+
if (map) {
103+
self.hasSetMap = YES;
104+
}
105+
}
106+
@end
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2013 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@import google_maps_flutter_ios;
6+
@import google_maps_flutter_ios.Test;
7+
@import XCTest;
8+
@import GoogleMaps;
9+
10+
#import "PartiallyMockedMapView.h"
11+
12+
/// A GMSCircle that ensures that property updates are made before the map is set.
13+
@interface PropertyOrderValidatingCircle : GMSCircle {
14+
}
15+
@property(nonatomic) BOOL hasSetMap;
16+
@end
17+
18+
@interface GoogleMapsCircleControllerTests : XCTestCase
19+
@end
20+
21+
@implementation GoogleMapsCircleControllerTests
22+
23+
- (void)testUpdateCircleSetsVisibilityLast {
24+
PropertyOrderValidatingCircle *circle = [[PropertyOrderValidatingCircle alloc] init];
25+
[FLTGoogleMapCircleController
26+
updateCircle:circle
27+
fromPlatformCircle:[FGMPlatformCircle
28+
makeWithConsumeTapEvents:NO
29+
fillColor:0
30+
strokeColor:0
31+
visible:YES
32+
strokeWidth:0
33+
zIndex:0
34+
center:[FGMPlatformLatLng makeWithLatitude:0
35+
longitude:0]
36+
radius:10
37+
circleId:@"circle"]
38+
withMapView:[GoogleMapsCircleControllerTests mapView]];
39+
XCTAssertTrue(circle.hasSetMap);
40+
}
41+
42+
/// Returns a simple map view to add map objects to.
43+
+ (GMSMapView *)mapView {
44+
GMSMapViewOptions *mapViewOptions = [[GMSMapViewOptions alloc] init];
45+
mapViewOptions.frame = CGRectMake(0, 0, 100, 100);
46+
mapViewOptions.camera = [[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0];
47+
return [[PartiallyMockedMapView alloc] initWithOptions:mapViewOptions];
48+
}
49+
50+
@end
51+
52+
@implementation PropertyOrderValidatingCircle
53+
- (void)setPosition:(CLLocationCoordinate2D)position {
54+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
55+
super.position = position;
56+
}
57+
58+
- (void)setRadius:(CLLocationDistance)radius {
59+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
60+
super.radius = radius;
61+
}
62+
63+
- (void)setStrokeWidth:(CGFloat)strokeWidth {
64+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
65+
super.strokeWidth = strokeWidth;
66+
}
67+
68+
- (void)setStrokeColor:(UIColor *)strokeColor {
69+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
70+
super.strokeColor = strokeColor;
71+
}
72+
73+
- (void)setFillColor:(UIColor *)fillColor {
74+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
75+
super.fillColor = fillColor;
76+
}
77+
78+
- (void)setTitle:(NSString *)title {
79+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
80+
super.title = title;
81+
}
82+
83+
- (void)setTappable:(BOOL)tappable {
84+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
85+
super.tappable = tappable;
86+
}
87+
88+
- (void)setZIndex:(int)zIndex {
89+
XCTAssertFalse(self.hasSetMap, @"Property set after map was set.");
90+
super.zIndex = zIndex;
91+
}
92+
93+
- (void)setMap:(GMSMapView *)map {
94+
// Don't actually set the map, since that requires more test setup.
95+
if (map) {
96+
self.hasSetMap = YES;
97+
}
98+
}
99+
@end

0 commit comments

Comments
 (0)