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

[ios, macos] possibility to set custom images to style #6637

Merged
merged 12 commits into from
Oct 12, 2016
Merged

[ios, macos] possibility to set custom images to style #6637

merged 12 commits into from
Oct 12, 2016

Conversation

rmnblm
Copy link

@rmnblm rmnblm commented Oct 9, 2016

With the changes to the core in #6375 we can now add custom images to the sprite atlas. This PR resolves #6436 to add the possibility to add custom images from bundles in iOS and macOS.

As proposed from @1ec5 in #6436, I've first named the method setImage:forName: but to stay consistent with all other methods of MGLStyle.h I've decided to name the method addImage:forName:.

Usage:

let imageURL = Bundle.main.url(forResource: "animal-dog", withExtension: "png")
mapView.style().addImage(imageURL!.path, forName: "dog")

let styleLayer  = MGLSymbolStyleLayer(identifier: "restaurants", source: geoJSONSource)
styleLayer.predicate = NSPredicate(format: "amenity == 'restaurant'")
styleLayer.iconImage = "dog" as MGLStyleAttributeValue!
mapView.style().add(styleLayer)

img_0011

Remarks:
This being my first PR and I'm relatively new to the codebase of Mapbox GL Native I hope I did everything correctly following the code and contribution guidelines.

During development I had to decide what is the argument type for the image being passed to
As we all know, to use images from a .xcassets folder, you can use NSImage on macOS or UIImage on iOS but there is no base class for both to use as an argument type in

- (void)addImage:(  -->  NSImage/UIImage  <--   *)image forName:(NSString *)name

That's why I've decided to use a NSString * which is the absolute path of the image. This solution implies that you only can use images part of the bundle and not part of a .xcassets folder.

- (void)addImage:(NSString *)imagePath forName:(NSString *)name

/cc @nitrag

@mention-bot
Copy link

@rmnblm, thanks for your PR! By analyzing the history of the files in this pull request, we identified @1ec5, @frederoni and @incanus to be potential reviewers.

@1ec5
Copy link
Contributor

1ec5 commented Oct 9, 2016

As we all know, to use images from a .xcassets folder, you can use NSImage on macOS or UIImage on iOS but there is no base class for both to use as an argument type

MGLTypes.h declares MGLColor as a typedef of NSColor on macOS and UIColor on iOS. Alternatively, you can use conditional compilation (#if) to declare two variations of the method, one per platform. It would be better for the method to take an image object than a path: after all, an NSColor or UIColor can be generated programmatically, without involving any file.

@1ec5
Copy link
Contributor

1ec5 commented Oct 9, 2016

I've first named the method setImage:forName: but to stay consistent with all other methods of MGLStyle.h I've decided to name the method addImage:forName:.

Methods beginning with “add” typically append an object to an ordered collection, as in the case of -[NSMutableArray addObject:] or indeed -[MGLStyle addLayer:], or add an object to an unordered collection, as in the case of -[MGLMutableSet addObject:] or -[MGLStyle addSource:]. In these cases, either the objects lack unique identifiers or the unique identifier is part of the object being added (MGLStyleLayer, MGLSource). By contrast, the style image API you’re implementing is adding an image to an unordered collection whose keys are external to the images. In that sense, it’s analogous to NSMutableDictionary and NSUserDefaults, which have -setObject:forKey: methods.

A -setImage:forKey: method would accept nil to remove an image, so a separate -removeImage: method would be unnecessary.

@rmnblm
Copy link
Author

rmnblm commented Oct 9, 2016

Good point about the "add" in front of methods and I totally agree with out but then again, it's inconsistent with the map.hpp/map.cpp names

void addImage(const std::string&, std::unique_ptr<const SpriteImage>);
void removeImage(const std::string&);

while the implementation of addImage does indeed set the sprite in the atlas

impl->style->spriteAtlas->setSprite(name, std::move(image));

A -setImage:forKey: method would accept nil to remove an image, so a separate -removeImage: method would be unnecessary.

I can do this but in my opinion this would be misleading. Wouldn't it be better to also follow the API convention of a NSMutableDictionary removeObjectForKey:

- (void)removeImageForName:(NSString *)name;

@rmnblm
Copy link
Author

rmnblm commented Oct 9, 2016

I just pushed commit 762f0a39574606f29d670f688ed5fba6c7bee83a with the following changes:

  • I added a new type inside MGLTypes.h called MGLImage which is either an UIImage on iOS or a NSImage on macOS

  • I renamed the method as you proposed, now there are

    - (void)setImage:(MGLImage *)image forName:(NSString *)name;
    - (void)removeImageForName:(NSString *)name;
    
  • I reused the code from MGLMapView.mm to get a mbgl::SpriteImage from a UIImage/NSImage. This was done by adding two new source files for iOS (UIImage+MGLAdditions) and macOS (NSImage+MGLAdditions) each.

  • I refactored MGLMapView.mm in both iOS and macOS to use the new implementations to avoid duplicate code

Usage:

let image = UIImage(named: "animal-dog")!
mapView.style().setImage(image, forName: "dog")

let styleLayer = MGLSymbolStyleLayer(identifier: "restaurants", source: geoJSONSource)
styleLayer.predicate = NSPredicate(format: "amenity == 'restaurant'")
styleLayer.iconImage = "dog" as MGLStyleAttributeValue!
mapView.style().add(styleLayer)

@@ -258,6 +258,21 @@ static const NSInteger MGLStyleDefaultVersion = 9;
*/
- (void)removeStyleClass:(NSString *)styleClass;

/**
Sets an image to the sprite atlas with a unique identifier.
Copy link
Contributor

Choose a reason for hiding this comment

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

“Sprite atlas” is jargon that most developers would find unintuitive. It’s probably sufficient to describe the image being added to the style (since it lasts until the style URL changes), then mention that the developer can use the image by setting the MGLSymbolStyleLayer.iconImage property to the name passed in here.

/**
Sets an image to the sprite atlas with a unique identifier.

@param image
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to document what image is.

@@ -254,5 +257,21 @@ - (void)removeStyleClass:(NSString *)styleClass
}
}

- (void)setImage:(MGLImage *)image forName:(NSString *)name
{
if (image && name)
Copy link
Contributor

Choose a reason for hiding this comment

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

Assert instead of failing gracefully. These two parameters are implied to be nonnull in the header.

Copy link
Author

@rmnblm rmnblm Oct 10, 2016

Choose a reason for hiding this comment

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

You're right, I did not see that. Would it be sufficient to remove the if-Statement?
As a side note: Isn't this if-Statement also unnecessary?

Copy link
Contributor

Choose a reason for hiding this comment

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

By default, passing in nil for a nonnull parameter is only a warning rather than an error, so NSAssert() is for the benefit of developers who ignore their warnings. (Tsk, tsk!) On the other hand, we haven’t been disciplined about asserting in similar cases – -addStyleClass: and -removeStyleClass: are more examples of this – so it’s a bit unfair of me to nitpick about this detail. 😊

Copy link
Author

@rmnblm rmnblm Oct 10, 2016

Choose a reason for hiding this comment

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

I'm glad you're taking it seriously and don't merge everything without some code review and I'm thankful for your nitpicking because I can only benefit from it. ☺️ Going to push all changes in a few minutes.

Copy link
Author

Choose a reason for hiding this comment

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

By the way: Do I have to NSAssert() now inside both methods because (MGLImage *)image and (NSString *)name can't be null anyway and as a side quesstion: What is your recommended way to assert for null?

NSAssert([name isEqual:[NSNull null]], "Name cannot be null");

or 

NSAssert(name == NULL, "Name cannot be null");

Copy link
Contributor

@1ec5 1ec5 Oct 10, 2016

Choose a reason for hiding this comment

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

The first form will crash unless name is [NSNull null] specifically. Since name is of type NSString * rather than id, there is no way to avoid the crash.

The second form will crash unless name is NULL, which is the opposite of what you’d do in this case. Moreover, in Objective-C, nil is conventionally used for Objective-C objects, NULL is used for C pointers and double-pointers, and Nil is used for Objective-C Classes. I recommend reading this NSHipster article to learn about the various ways to say “nothing” in Objective-C.

In this case, you can simply NSAssert(name).

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the article! If I can follow, you're expecting this:

NSAssert(image);
NSAssert(name);

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep.

Copy link
Author

Choose a reason for hiding this comment

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

Just pushed it. I had to add a second argument with description otherwise I got error too few arguments provided to function-like macro invocation.


- (void)removeImageForName:(NSString *)name
{
if (name)
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly, assert here.

@@ -3,6 +3,14 @@
#pragma once

#if TARGET_OS_IPHONE
@class UIImage;
Copy link
Contributor

@1ec5 1ec5 Oct 10, 2016

Choose a reason for hiding this comment

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

👍

(Sorry, for some reason I thought we had already defined MGLImage, but in fact it was MGLColor that we implemented.)

@@ -5,6 +5,12 @@
#import <mbgl/util/default_styles.hpp>
#include <mbgl/mbgl.hpp>

#if TARGET_OS_IPHONE
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 move this over to MGLStyle.mm?

@@ -1,5 +1,7 @@
#import "MGLStyle.h"

#import "MGLTypes.h"
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be unnecessary as long as MGLStyle.h imports MGLTypes.h.

@@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
30E5781B1DAA857E0050F07E /* NSImage+MGLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 30E578141DAA7D920050F07E /* NSImage+MGLAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; };
Copy link
Contributor

Choose a reason for hiding this comment

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

This header’s visibility should be Project, not Public. Making it public would expose C++ syntax to pure Objective-C projects using the SDK.

Copy link
Author

Choose a reason for hiding this comment

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

I first had UIImage+MGLAdditions/NSImage+MGLAdditions public but then realized it wasn't necessary, might have missed to set it back to Project here.

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.

Thanks for the revisions. You’re right about having a separate -removeImageForName:, and putting the SpriteImage conversion in a category is a great idea.


@interface UIImage (MGLAdditions)

+ (std::unique_ptr<mbgl::SpriteImage>)mbgl_spriteImageFromImage:(UIImage *)image;
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be more natural to have an instance method in this case: -mgl_spriteImage. (Also, for consistency’s sake, the class prefix is mgl rather than mbgl.)

@rmnblm
Copy link
Author

rmnblm commented Oct 10, 2016

I just noticed that I wasn't consistent with my last two commit messages (delete/remove). 😑

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.

Almost there!

Sets an image to the sprite atlas with a unique identifier.
Sets the name and image to the style.
The image can be used by setting the MGLSymbolStyleLayer.iconImage
property to the name passed in here
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 can be used to both add a new image or override an existing one, so we should make that clear. Also, when jazzy generates the HTML documentation, it automatically links symbols enclosed in backticks, to help readers navigate the multi-page documentation. This wording would be more verbose, but it leaves nothing open to interpretation:

Adds or overrides an image used by the style’s layers.

To use an image in a style layer, give it a unique name using this method, then set the iconImage property of an MGLSymbolStyleLayer object to that name.

@@ -4,6 +4,6 @@

@interface UIImage (MGLAdditions)

+ (std::unique_ptr<mbgl::SpriteImage>)mbgl_spriteImageFromImage:(UIImage *)image;
+ (std::unique_ptr<mbgl::SpriteImage>)mgl_spriteImage:(UIImage *)image;
Copy link
Contributor

@1ec5 1ec5 Oct 10, 2016

Choose a reason for hiding this comment

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

What I meant was that this would become a parameter-less instance method, like this:

- (std::unique_ptr<mbgl::SpriteImage>)mgl_spriteImage;

Within the implementation of an instance method, we have access to all the members of self, such as self.CGImage. Then you can simply say annotationImage.image.mgl_spriteImage to get the unique_ptr.

Copy link
Author

Choose a reason for hiding this comment

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

Oops, now I know what you meant. Needed more coffee to understand it. ☕️

@1ec5
Copy link
Contributor

1ec5 commented Oct 10, 2016

I just noticed that I wasn't consistent with my last two commit messages (delete/remove). 😑

No worries. Normally we use GitHub’s “Squash and merge” feature, so the title of this PR will become the first line of the squashed commit message. If it bothers you, though, you can use git rebase -i to revise your commit messages, then git push --force-with-lease to override what you’ve previously pushed to this branch.

@1ec5
Copy link
Contributor

1ec5 commented Oct 10, 2016

It’s a bit unusual to have a setter and remover without an -imageForName: getter. mbgl::Map doesn’t currently expose a way to get an installed image, so that’ll be a more invasive change. This getter wouldn’t be a high priority, so it’s fine to punt on it for now.

@rmnblm
Copy link
Author

rmnblm commented Oct 10, 2016

I also thought about a getter like -imageForName but knew that there wasn't such an implementation in mbgl::Map. Though it's unusual to have a setter without a getter, I can't think of a use case where I'd need such getter. But somebody might need it in the future, who knows... 🌝

@1ec5 1ec5 added feature iOS Mapbox Maps SDK for iOS macOS Mapbox Maps SDK for macOS runtime styling labels Oct 11, 2016
@1ec5 1ec5 added this to the ios-v3.4.0 milestone Oct 11, 2016
@1ec5
Copy link
Contributor

1ec5 commented Oct 11, 2016

Sorry, this ended up conflicting with #6636, which just got merged. Can you resolve the (minor) conflict or check the “Allow edits from maintainers” box so I push the resolution before merging? Thanks!

@rmnblm
Copy link
Author

rmnblm commented Oct 12, 2016

I enabled edits from maintainers, the stage is yours 🏟

@1ec5 1ec5 merged commit a24e559 into mapbox:master Oct 12, 2016
@1ec5
Copy link
Contributor

1ec5 commented Oct 12, 2016

Thanks! 🎉

@rmnblm rmnblm deleted the custom_style_images branch October 12, 2016 19:32
@friedbunny
Copy link
Contributor

friedbunny commented Oct 13, 2016

This commit to master failed to compile on CI:

 Compiling NSImage+MGLAdditions.mm

❌  /Users/vagrant/git/platform/macos/src/NSImage+MGLAdditions.mm:10:6: use of undeclared identifier 'image'

    [image lockFocus];
     ^



❌  /Users/vagrant/git/platform/macos/src/NSImage+MGLAdditions.mm:12:6: use of undeclared identifier 'image'

    [image unlockFocus];
     ^

1ec5 added a commit that referenced this pull request Oct 13, 2016
Followup to last-minute refactoring in #6637.
@1ec5
Copy link
Contributor

1ec5 commented Oct 13, 2016

Thanks @friedbunny. Bitrise CI doesn’t run on third-party PRs, unfortunately. Incoming fix in #6690.

1ec5 added a commit that referenced this pull request Oct 13, 2016
Followup to last-minute refactoring in #6637.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature iOS Mapbox Maps SDK for iOS macOS Mapbox Maps SDK for macOS runtime styling
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add API for managing a style’s icon images
4 participants