Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Add convenience initializers to shape source #7665

Merged
merged 1 commit into from
Jan 12, 2017

Conversation

boundsj
Copy link
Contributor

@boundsj boundsj commented Jan 10, 2017

Fixes #7453

This adds two new convenience initializers to MGLShapeSource:

  • -initWithIdentifier:features:options: takes an array of shape objects that conform to MGLFeature, inserts them in a shape collection feature, and creates a source with that shape.

  • -initWithIdentifier:shapes:options: does the same but with concrete MGLShape objects that get added to a shape collection.

Although both are technically convenience initializers, developers will most often want to use -initWithIdentifier:features:options: since features objects unlock key runtime styling functionality. So, the inline example has been updated to demonstrate MGLShapeSource usage with -initWithIdentifier:features:options:

The type system will help ensure that Swift developers use the correct convenience initializer for objects that are features (i.e. it is not possible to send plain shapes to the features initializer without an unsafe cast). There is currently no warning system for this though so Objective-C developers will not be protected from bugs where layer properties or predicate filters don't work as expected because plain shapes were previously used with the features initializer.

cc @frederoni @bsudekum @ericrwolfe

@boundsj boundsj added iOS Mapbox Maps SDK for iOS macOS Mapbox Maps SDK for macOS runtime styling labels Jan 10, 2017
@boundsj boundsj added this to the ios-v3.4.0 milestone Jan 10, 2017
@boundsj boundsj self-assigned this Jan 10, 2017
@boundsj boundsj requested a review from 1ec5 January 10, 2017 21:29
@boundsj boundsj force-pushed the boundsj-add-shapesource-conveniences branch from bb0446f to 6b99f55 Compare January 10, 2017 21:29
@boundsj boundsj changed the title [ios, macos] Add convenience initializers to shape source Add convenience initializers to shape source Jan 10, 2017
@boundsj
Copy link
Contributor Author

boundsj commented Jan 10, 2017

Do we actually need a -initWithIdentifier:shapes:options:? It's nice that we support geometries when GeoJSON is loaded via a URL (or by the existing shape API) but I think, to avoid confusion, that we could get away with just -initWithIdentifier:features:options:.

@@ -56,6 +56,16 @@ - (instancetype)initWithIdentifier:(NSString *)identifier shape:(nullable MGLSha
return self;
}

- (instancetype)initWithIdentifier:(NSString *)identifier features:(NS_ARRAY_OF(MGLFeature) *)features options:(nullable NS_DICTIONARY_OF(MGLShapeSourceOption, id) *)options {
MGLShapeCollectionFeature *shapeCollectionFeature = [MGLShapeCollectionFeature shapeCollectionWithShapes:features];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we check the type of each object in features and log a one time warning if a non-feature makes it through?

Copy link
Contributor

Choose a reason for hiding this comment

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

Since lightweight generic qualifiers are completely optional in Objective-C, I think it’s fairly likely that a developer would incorrectly attempt to pass in an array of MGLShapes, especially if we get rid of -initWithIdentifier:shapes:options:. I think we should raise an exception in that case, since the value would contradict the type expected by this method.

Copy link
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

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

In addition to the comments below, refer to -initWithIdentifier:features:options: in the -initWithIdentifier:shape:options: initializer as a way to put multiple features in the same source.

@@ -143,6 +142,41 @@ extern const MGLShapeSourceOption MGLShapeSourceOptionSimplificationTolerance;
*/
- (instancetype)initWithIdentifier:(NSString *)identifier shape:(nullable MGLShape *)shape options:(nullable NS_DICTIONARY_OF(MGLShapeSourceOption, id) *)options NS_DESIGNATED_INITIALIZER;

/**
Returns a shape source with an identifier, a shape that is a
`MGLShapeCollectionFeature` holding the features passed in, and dictionary of
Copy link
Contributor

Choose a reason for hiding this comment

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

This method takes an array of features, not a single shape collection feature object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does take an array but that is not what it returns in the "shape" property of the instantiated object. Isn't that what this sentence is trying to explain?

Copy link
Contributor

Choose a reason for hiding this comment

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

This sentence explains what you pass into the method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah of course! I think not seeing "initialized with" threw me off for some reason today :D

`MGLShapeCollectionFeature` holding the features passed in, and dictionary of
options for the source.

Passing an array of `MGLShape` subclass instances that conform to the
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a given.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe this could be a @note instead? My thinking is that we should take every chance we have to point out the subtle difference between shapes and features. If we do get rid of -initWithIdentifier:shapes:options: then this note could probably go away completely.

Copy link
Contributor

@1ec5 1ec5 Jan 10, 2017

Choose a reason for hiding this comment

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

Unlike -initWithIdentifier:shapes:options:, this method accepts MGLFeature instances, such as MGLPointFeature objects, whose attributes you can use when applying a predicate to MGLVectorStyleLayer or configuring a style layer’s appearance.


/**
Returns a shape source with an identifier, a shape that is a
`MGLShapeCollection` holding the shapes passed in, and dictionary of
Copy link
Contributor

Choose a reason for hiding this comment

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

This method takes an array of shapes, not a single shape collection object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as #7665 (comment)

`+[MGLShape shapeWithData:encoding:error:]` method.

@param identifier A string that uniquely identifies the source.
@param features An array of `MGLShape` subclass instances that conform to the `MGLFeature` protocol
Copy link
Contributor

Choose a reason for hiding this comment

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

An array of objects that conform to the MGLFeature protocol.

`+[MGLShape shapeWithData:encoding:error:]` method.

@param identifier A string that uniquely identifies the source.
@param shapes An array of `MGLShape` subclass instances
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing a period, and “subclass instances” is a bit confusing:

An array of shapes; each shape is a member of a concrete subclass of MGLShape.

@@ -56,6 +56,16 @@ - (instancetype)initWithIdentifier:(NSString *)identifier shape:(nullable MGLSha
return self;
}

- (instancetype)initWithIdentifier:(NSString *)identifier features:(NS_ARRAY_OF(MGLFeature) *)features options:(nullable NS_DICTIONARY_OF(MGLShapeSourceOption, id) *)options {
Copy link
Contributor

Choose a reason for hiding this comment

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

NS_ARRAY_OF(MGLFeature) * resolves to NSArray<MGLFeature> *, meaning the passed-in value must be an NSArray object that conforms to the MGLFeature protocol. I think what you meant was NS_ARRAY_OF(id <MGLFeature>) *.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. The header is written that way but it did not make it here somehow.

@@ -56,6 +56,16 @@ - (instancetype)initWithIdentifier:(NSString *)identifier shape:(nullable MGLSha
return self;
}

- (instancetype)initWithIdentifier:(NSString *)identifier features:(NS_ARRAY_OF(MGLFeature) *)features options:(nullable NS_DICTIONARY_OF(MGLShapeSourceOption, id) *)options {
MGLShapeCollectionFeature *shapeCollectionFeature = [MGLShapeCollectionFeature shapeCollectionWithShapes:features];
Copy link
Contributor

Choose a reason for hiding this comment

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

Since lightweight generic qualifiers are completely optional in Objective-C, I think it’s fairly likely that a developer would incorrectly attempt to pass in an array of MGLShapes, especially if we get rid of -initWithIdentifier:shapes:options:. I think we should raise an exception in that case, since the value would contradict the type expected by this method.

MGLShapeCollectionFeature *shape = (MGLShapeCollectionFeature *)source.shape;

XCTAssertTrue([shape isKindOfClass:[MGLShapeCollectionFeature class]]);
XCTAssert(shape.shapes.count == 1, @"Shape collection should contain 1 shape");
Copy link
Contributor

Choose a reason for hiding this comment

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

Use XCTAssertEqual() (×2).

@1ec5 1ec5 added the refactor label Jan 10, 2017
@1ec5
Copy link
Contributor

1ec5 commented Jan 10, 2017

Also, can you see if the “Working with GeoJSON Data” guide needs any updates?

Returns a shape source with an identifier, a shape that is a
`MGLShapeCollection` holding the shapes passed in, and dictionary of
options for the source.

Copy link
Contributor

@1ec5 1ec5 Jan 10, 2017

Choose a reason for hiding this comment

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

Any MGLFeature instance passed into this initializer is treated as an ordinary shape, causing any attributes to be inaccessible to an MGLVectorStyleLayer when evaluating a predicate or configuring certain layout or paint attributes. To preserve the attributes associated with each feature, use the -initWithIdentifier:features:options: or -initWithIdentifier:shape:options: method instead.

@@ -143,6 +142,41 @@ extern const MGLShapeSourceOption MGLShapeSourceOptionSimplificationTolerance;
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

The documentation for this method currently says:

To include multiple shapes in the source, use an MGLShapeCollection or MGLShapeCollectionFeature object.

Change it to:

To include multiple shapes in the source, use an MGLShapeCollection or MGLShapeCollectionFeature object, or use the -initWithIdentifier:features:options: or -initWithIdentifierr:shapes:options: method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was done in 6ba0034

@boundsj boundsj force-pushed the boundsj-add-shapesource-conveniences branch from 3a6f2f4 to 6ba0034 Compare January 11, 2017 02:24
@boundsj
Copy link
Contributor Author

boundsj commented Jan 11, 2017

Updated the guide in 6ba0034

@@ -36,6 +36,17 @@ resulting shape or feature object into the
the map, or you can use the object and its properties to power non-map-related
functionality in your application.

To include multiple shapes in the source, create and pass an `MGLShapeCollection` or
`MGLShapeCollectionFeature` object to
`-[MGLShapeSource initWithIdentifier:shape:options:]`. Or, use the
Copy link
Contributor

Choose a reason for hiding this comment

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

s/Or/Alternatively/

`MGLShapeCollectionFeature` object to
`-[MGLShapeSource initWithIdentifier:shape:options:]`. Or, use the
`-[MGLShapeSource initWithIdentifier:features:options:]` or
`-[MGLShapeSource initWithIdentifier:shapes:options:]` methods to create shape
Copy link
Contributor

Choose a reason for hiding this comment

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

method to create a shape source with an array

[NSException raise:NSInvalidArgumentException format:@"The object %@ included in the features argument does not conform to the MGLFeature protocol.", feature];
}
}
MGLShapeCollectionFeature *shapeCollectionFeature = [MGLShapeCollectionFeature shapeCollectionWithShapes:(NSArray *)features];
Copy link
Contributor

Choose a reason for hiding this comment

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

You likely had to cast here because +[MGLShapeCollectionFeature shapeCollectionWithShapes:] accepts an array of MGLShape <MGLFeature> * instead of an array of id <MGLFeature>. Let’s make this method and +[MGLShapeCollectionFeature shapeCollectionWithShapes:] consistent with each other. (I have no strong preference one way or the other, but id <MGLFeature> might leave more options open for fixing #7454.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since MGLShapeCollectionFeature inherits from MGLShapeCollection and MGLShapeCollection uses an NS_ARRAY_OF(MGLShape *) I don't think I can change MGLShapeCollectionFeature since it would require the change to bubble up to MGLShapeCollection 😬

The implementation of MGLShapeCollectionFeature does not use lightweight generics (just a plain old NSArray). I made the same change to MGLShapeSource in 6287c6b. Are you ok with this solution for this case, for now?

Copy link
Contributor

Choose a reason for hiding this comment

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

The implementation of +[MGLShapeCollectionFeature shapeCollectionWithShapes:] uses an unqualified NSArray in its signature, but the declaration has a qualified NSArray<MGLShape<MGLFeature> *> *. Omitting the qualifier in the implementation only takes away opportunities for the compiler to catch mistakes. We should qualify as much as possible.

Since +[MGLShapeCollectionFeature shapeCollectionWithShapes:] is declared to take an NSArray<MGLShape<MGLFeature> *> * rather than the broader type NSArray<id<MGLFeature>> *, -[MGLShapeSource initWithIdentifier:features:options:] should take an NSArray<MGLShape<MGLFeature> *> * as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it. This is done in e85da7f. I'll merge after the build passes.

This adds two new convenience initializers to MGLShapeSource:

-initWithIdentifier:features:options: takes an array of shape objects
that conform to MGLFeature, inserts them in a shape collection feature
and creates a source with that shape. -initWithIdentifier:shapes:options
does the same but with concrete MGLShape objects that get added to
a shape collection.

Throw an exception if an shape source is created with the features
initializer but is sent an array of features that contains something
that is not actually an object that conforms to the feature protocol.

Updates to geojson data guide

Qualify APIs that take arrays of shapes that are features
@boundsj boundsj force-pushed the boundsj-add-shapesource-conveniences branch from e85da7f to 4166635 Compare January 11, 2017 19:51
@boundsj boundsj merged commit 6ad47cf into release-ios-v3.4.0 Jan 12, 2017
@boundsj boundsj deleted the boundsj-add-shapesource-conveniences branch January 12, 2017 00:05
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
iOS Mapbox Maps SDK for iOS macOS Mapbox Maps SDK for macOS refactor runtime styling
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants