Skip to content

Conversation

@stuartmorgan-g
Copy link
Collaborator

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

Footnotes

  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. 2 3

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the update logic for various map objects on iOS to fix a potential flickering issue. The changes ensure all properties of an object are set before it's added to the map, preventing a flicker of default values. This is achieved by introducing static helper methods for each map object type (markers, circles, polygons, etc.) that apply all properties first and then set the map property to make the object visible. This refactoring also resolves some init methods that were incorrectly calling instance methods. The changes are consistently applied across all relevant controllers and are well-supported by new unit tests that verify the correct property update order.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't actually deleted, I just renamed it (Polylines->Polyline) for consistency, and since I also add a lot of lines, git didn't see it as a rename.

mapViewOptions.frame = CGRectMake(0, 0, 100, 100);
mapViewOptions.camera = [[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0];
return [[PartiallyMockedMapView alloc] initWithOptions:mapViewOptions];
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could extract this as a convenience constructor, but it was simple enough that I just copied it from the tests that already did it into each other test file.


@end

@implementation PropertyOrderValidatingGroundOverlay
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered doing something sketchy with overriding core Obj-C selector handling methods to catch all property setters at once, instead of having boilerplate like this for every map object, but since that would have had to be rewritten to something like this to convert the tests to Swift I decided doing this was the better option.

self.groundOverlay.map = nil;
}

- (void)setConsumeTapEvents:(BOOL)consumes {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this refactoring in all of the object files: every object had a bunch of these passthroughs, almost all of which were a single line, and they are all private and only called by the updateFrom* method, so it seemed like a lot of boilerplate for nothing, and it also prevented me from making the update method a class method. It's not necessary to fix the issue, but it makes the test setup much simpler.

usingBounds:self.createdWithBounds];
}

+ (void)updateGroundOverlay:(GMSGroundOverlay *)groundOverlay
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the other part of the refactoring I made in every object. Having the class method helps with testing because I can expose this and not need to make a full controller (which for some objects has side effects on the map, which we aren't fully mocking, and it can get complicated). We also had a case where the controller's init* method called updateFrom*, which violates style, and making the class method allowed trivially fixing that.

[self setFadeIn:overlay.fadeIn];
[self setTileSize:overlay.tileSize];
// This must be done last, to avoid visual flickers of default property values.
tileLayer.map = platformOverlay.visible ? mapView : nil;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual fix; about half the objects were setting visibility somewhere other than the end.


#import "messages.g.h"

NS_ASSUME_NONNULL_BEGIN
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by fix; most of the headers had it, and I noticed this one was missing it.

#import "FLTGoogleMapJSONConversions.h"

/// Converts a list of holes represented as CLLocation lists to GMSMutablePath lists.
static NSArray<GMSMutablePath *> *FMGPathHolesFromLocationHoles(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted this helper rather than inlining all of setHoles:. It's shorter than the original because the inner loop was doing exactly what the existing FGMGetPathFromPoints utility method (in the utils file) already does.

identifier:identifier
mapView:self.mapView];
[controller updateFromPlatformPolygon:polygon registrar:self.registrar];
[controller updateFromPlatformPolygon:polygon];
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The registrar was either copypasta from another object, or it used to be needed and now isn't; it was unused, so I removed it in the refactoring.

///
/// @param styles The styles for repeating pattern sections.
/// @param lengths The lengths for repeating pattern sections.
- (void)setPattern:(NSArray<GMSStrokeStyle *> *)styles lengths:(NSArray<NSNumber *> *)lengths;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was exposed for testing, but since I inlined it the test now uses updatePolygon:... instead. That also means it's testing more of the codepath.

@stuartmorgan-g stuartmorgan-g added the triage-ios Should be looked at in iOS triage label Nov 11, 2025
Copy link
Contributor

@vashworth vashworth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with a few comments

withMapView:self.mapView
registrar:registrar
screenScale:screenScale
usingBounds:self.createdWithBounds];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if there's any reason to have a separately named getter? I'm assuming you can get it either way, since your code does here

@property(nonatomic, assign, getter=isCreatedWithBounds) BOOL createdWithBounds;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a style guide thing. From the Google Obj-C style guide:

Accessors that return the value of boolean adjectives have method names beginning with is, but property names for those methods omit the is.

Dot notation is used only with property names, not with method names.

// GOOD:

@property(nonatomic, getter=isGlorious) BOOL glorious;
// The method for the getter of the property above is:
// - (BOOL)isGlorious;

BOOL isGood = object.glorious;      // GOOD.
BOOL isGood = [object isGlorious];  // GOOD.
// AVOID:

BOOL isGood = object.isGlorious;    // AVOID.

@stuartmorgan-g stuartmorgan-g added the autosubmit Merge PR when tree becomes green via auto submit App label Nov 13, 2025
@auto-submit auto-submit bot merged commit 1e7a00a into flutter:main Nov 13, 2025
79 of 80 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App p: google_maps_flutter platform-ios triage-ios Should be looked at in iOS triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[google_maps_flutter] Polygons flash blue on iOS

2 participants