Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Example] Data-Driven, Anchored Styling #548

Closed
wants to merge 17 commits into from

Conversation

cmilack
Copy link
Contributor

@cmilack cmilack commented Jul 16, 2021

Pull request checklist:

  • Briefly describe the changes in this PR.

Summary of changes

This PR adds a new top-level example to the Examples app. This example shows how to add a custom SymbolLayer that supports adding data-driven image annotations that support anchoring on either the left, center, or right side of the annotation image. Annotations support stretchable images that will dynamically resize based on their text contents.

This was originally submitted another PR which has been closed: #190. All feedback from that PR has been addressed here.

This also addresses the issue described here: #381. This turned out to be expected behavior when using both icon-anchor and icon-text-fit.

User impact (optional)

Custom Symbols

@CLAassistant
Copy link

CLAassistant commented Jul 16, 2021

CLA assistant check
All committers have signed the CLA.

avi-c
avi-c previously approved these changes Jul 16, 2021
Copy link

@avi-c avi-c left a comment

Choose a reason for hiding this comment

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

I'm approving although it would be good to get another reviewer too since I wrote a bunch of the code I am approving. That said, most of the code was already approved in the earlier, abandoned PR so I think it should be approved by other reviewers without any major issues.

Comment on lines 313 to 314
// swiftlint:enable all No newline at end of file
// swiftlint:enable all
Copy link
Contributor

Choose a reason for hiding this comment

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

probably shouldn't be touching generated files

}

@objc(CustomAnchoredSymbolExample)
public class CustomAnchoredSymbolExample: UIViewController, ExampleProtocol {
Copy link
Contributor

Choose a reason for hiding this comment

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

App code never needs to be public since it won't be consumed by other modules. Keeping it internal unlocks additional compile-time optimizations that are not possible for public constructs.

case center
}

private struct DebugFeature {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we choose a different name here? What's Debug about this feature?

Copy link

Choose a reason for hiding this comment

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

Another name is fun. It was originally named debug since it was for debugging the visualization of the annotations and "Feature" was a reserved word. Maybe "AnchoredFeature" or similar is better now.

Copy link
Contributor

Choose a reason for hiding this comment

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

See #548 (comment). I think avoiding the term feature in this name would be advantageous.

let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector((mapSymbolTap(sender:))))
mapView.addGestureRecognizer(tapGestureRecognizer)

// Allows the delegate to receive information about map events.
Copy link
Contributor

Choose a reason for hiding this comment

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

Use of the word delegate in this comment is confusing given that this doesn't really fit the typical delegation pattern on iOS

}
}

private func addFeatures() -> FeatureCollection {
Copy link
Contributor

Choose a reason for hiding this comment

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

The naming of this method implies side effects, but there aren't any. How about makeFeatures instead, following the Swift API naming conventions?

Screen Shot 2021-07-20 at 11 36 01 AM

https://swift.org/documentation/api-design-guidelines/

self.finish()
}

// Add the label on top of the map view controller.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Add the label on top of the map view controller.
// Add the label on top of the map view.

extension UIImage {
// Produce a copy of the image with tint color applied.
// Useful for deployment to iOS versions prior to 13 where tinting support was added to UIImage natively.
func tint(_ tintColor: UIColor) -> UIImage {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
func tint(_ tintColor: UIColor) -> UIImage {
func tinted(with tintColor: UIColor) -> UIImage {
  • tinted vs tint: The latter implies mutation. The former implies the creation of a new object.
  • _ vs with: without the argument label, at the point of use, it reads as if you're tinting the color rather than the image.

Comment on lines +295 to +296
// Produce a copy of the image with tint color applied.
// Useful for deployment to iOS versions prior to 13 where tinting support was added to UIImage natively.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we do some availability checks inside this method and use the built-in method when available? Then when we drop support for iOS 12 and lower someday, Xcode should lead us here with a warning and we can remove this extension entirely

return FeatureCollection(features: features)
}

private func addSymbolLayer(features: FeatureCollection) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: features implies [Feature] which is only 1 aspect of a FeatureCollection.

Suggested change
private func addSymbolLayer(features: FeatureCollection) {
private func addSymbolLayer(with featureCollection: FeatureCollection) {

}

@objc private func mapSymbolTap(sender: UITapGestureRecognizer) {
if sender.state == .recognized {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider converting to a guard


@objc(CustomAnchoredSymbolExample)
public class CustomAnchoredSymbolExample: UIViewController, ExampleProtocol {
static let symbols = "custom_symbols"
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we improve the naming of this constant? Is it an identifier?

Copy link
Contributor

Choose a reason for hiding this comment

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

It also seems unnecessary for this to be static. Letting it be an instance property would clean things up at the point of use.

Comment on lines +168 to +177
var features = [Turf.Feature]()

let featureList = [
DebugFeature(coordinate: CLLocationCoordinate2DMake(40.714203, -74.006314), highlighted: false, sortOrder: 0, tailPosition: .left, label: "Chambers & Broadway - Lefthand Stem", imageName: "imageLeftHanded"),
DebugFeature(coordinate: CLLocationCoordinate2DMake(40.707918, -74.006008), highlighted: false, sortOrder: 0, tailPosition: .right, label: "Cliff & John - Righthand Stem", imageName: "imageRightHanded"),
DebugFeature(coordinate: CLLocationCoordinate2DMake(40.716281, -74.004526), highlighted: true, sortOrder: 1, tailPosition: .right, label: "Broadway & Worth - Right Highlighted", imageName: "imageRightHanded-Highlighted"),
DebugFeature(coordinate: CLLocationCoordinate2DMake(40.710194, -74.004248), highlighted: true, sortOrder: 1, tailPosition: .left, label: "Spruce & Gold - Left Highlighted", imageName: "imageLeftHanded-Highlighted"),
DebugFeature(coordinate: CLLocationCoordinate2DMake(40.7128, -74.0060), highlighted: true, sortOrder: 2, tailPosition: .center, label: "City Hall - Centered Highlighted", imageName: "imageCentered-Highlighted"),
DebugFeature(coordinate: CLLocationCoordinate2DMake(40.711427, -74.008614), highlighted: false, sortOrder: 3, tailPosition: .center, label: "Broadway & Vesey - Centered Stem", imageName: "imageCentered")
]
Copy link
Contributor

Choose a reason for hiding this comment

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

Calling the lower array featureList is confusing given that now we have Turf.Feature and DebugFeature. Maybe we could rename DebugFeature in terms of just some generic data entity and avoid using the term feature in its name

Comment on lines +179 to +180
for (index, feature) in featureList.enumerated() {
let point = Turf.Point(feature.coordinate)
Copy link
Contributor

Choose a reason for hiding this comment

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

With https://github.com/mapbox/mapbox-maps-ios/pull/548/files#r673294649, in mind, maybe this would become something like…

Suggested change
for (index, feature) in featureList.enumerated() {
let point = Turf.Point(feature.coordinate)
for (index, dataItem) in dataItems.enumerated() {
let point = Turf.Point(dataItem.coordinate)

Comment on lines +181 to +190
var pointFeature = Feature(geometry: .point(point))

// Set the feature attributes which will be used in styling the symbol style layer.
pointFeature.properties = ["highlighted": feature.highlighted,
"tailPosition": feature.tailPosition.rawValue,
"text": feature.label,
"imageName": feature.imageName,
"sortOrder": feature.highlighted == true ? index : -index]

features.append(pointFeature)
Copy link
Contributor

Choose a reason for hiding this comment

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

And then down here things get a lot more clear because we don't have to parse feature vs pointFeature

options: RenderedQueryOptions(layerIds: layers, filter: nil)) { result in
switch result {
case .success(let features):
if features.count > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if features.count > 0 {
if !features.isEmpty {

See docs on isEmpty for the recommended best practice

Comment on lines +89 to +90
if features.count > 0 {
guard let featureText = features[0].feature.properties["text"] as? String else { return }
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if features.count > 0 {
guard let featureText = features[0].feature.properties["text"] as? String else { return }
if let featureText = features.first?.feature.properties["text"] as? String {

Copy link
Contributor

@macdrevx macdrevx left a comment

Choose a reason for hiding this comment

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

Done with comments. Will look again after updates are in. Also happy to discuss feedback if desired.

Copy link
Contributor

@macdrevx macdrevx left a comment

Choose a reason for hiding this comment

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

Meant to request changes

@macdrevx macdrevx dismissed stale reviews from avi-c and themself July 29, 2021 15:58

Dismissing all reviews to ensure another review without blocking this PR while I'm away.

@macdrevx macdrevx self-assigned this Aug 10, 2021
@macdrevx
Copy link
Contributor

Closing this PR for now. We can revisit when we prioritize adding support for stretchable UIImages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants