Build a station finder, part 3

In part 2, we added markers to the map by parsing a GeoJSON file, extracted the data points from the file, and created markers for each Metro station. Now, in part 3, we will customize the markers, add a button to launch a modal web page, and create a custom view for our markers to show the line colors.

Improving our markers

Our app is working nicely, but let’s add some flair to our markers. We’ll add a custom rail car icon to the markers and customize the callout to show the colors of the lines that the station supports. We’ll also add an info button to bring up that station’s web page showing realtime arrivals.

When we’re done, our callouts will look like this:

Callout

Providing a layer for the annotation

To customize the callouts, we’re going to need to provide a custom layer for our annotation. This is because the SDK lets you customize marker callouts during customization of the marker. The RMMapViewDelegate method -mapView:layerForAnnotation: will be where we customize our marker.

First, we’ll tell our view controller that we conform to the RMMapViewDelegate protocol. Update the interface at the top of our ViewController.m:

@interface ViewController () <RMMapViewDelegate>

All of this protocol’s methods are optional, so we can implement only the ones we care about.

In our -viewDidLoad method, set the delegate property of our map view to self after instantiating it. This ensures that ViewController is the delegate of mapView and gets asked to make decisions about it:

self.mapView.delegate = self;

Finally, implement the -mapView:layerForAnnotation: delegate method by adding this to ViewController.m:

- (RMMapLayer *)mapView:(RMMapView *)mapView layerForAnnotation:(RMAnnotation *)annotation
{
    UIColor *metroBlue = [UIColor colorWithRed:0.01 green:0.22 blue:0.41 alpha:1];
    RMMarker *marker = [[RMMarker alloc] initWithMapboxMarkerImage:@"rail-metro"
                                                         tintColor:metroBlue];

    // We should show a callout when the user taps
    marker.canShowCallout = YES;

    return marker;
}

We are instantiating an RMMarker layer with a metro icon and a blue color from the WMATA website.

We set the canShowCallout property to YES so that a callout shows when we tap on the marker.

To use this new delegate method, we have to change the kind of annotation that we are creating. What we’re using now, RMPointAnnotation, provides a layer automatically with very basic configuration options. We’re going to change it to use the superclass, RMAnnotation.

Find the line where we create the RMPointAnnotation inside the -loadStations method:

RMPointAnnotation *stationAnnotation =
    [RMPointAnnotation annotationWithMapView:_mapView
                                  coordinate:coordinate
                                    andTitle:properties[@"title"]];

Change RMPointAnnotation to RMAnnotation:

RMAnnotation *stationAnnotation =
     [RMAnnotation annotationWithMapView:_mapView
                              coordinate:coordinate
                                andTitle:properties[@"title"]];

Build and run, and you should see custom blue markers that show a callout when tapped.

Screenshot

Customizing the callout

Our callout accepts accessory views on the left and right sides, so let’s use those to show a UIButton and a custom view for line colors. We will add a UIButton to the right side first.

Add this line to our -mapView:layerForAnnotation: method:

marker.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];

Build and run, and tap a callout. You should see a button on the right side of your callout like this:

Screenshot

Tapping the button doesn’t do anything yet, so we need to implement another RMMapViewDelegate method called -tapOnCalloutAccessoryControl:forAnnotation:onMap: that handles the tap.

Add this method to our ViewController.m:

- (void)tapOnCalloutAccessoryControl:(UIControl *)control forAnnotation:(RMAnnotation *)annotation onMap:(RMMapView *)map
{
    NSString *urlString = annotation.userInfo[@"url"];
    NSLog(@"URL: %@", urlString);
}

Build and run, then tap the button on one of your marker callouts. You should see a console log message that shows the URL for that annotation. (Recall that we assigned the properties object from our GeoJSON feature to the annotation’s userInfo property.)

Let’s show a modal view controller with a web view when the user taps the button on the callout. We are going to do it programmatically here, but you are welcome to use Storyboards if you wish.

Create a new file in our project by clicking File → New → File and then selecting Cocoa Touch Class from the iOS Source panel. Name the class WebViewController and set it as a subclass of UIViewController.

new file

Add a stationURL property to the @interface section of the WebViewController.h header file. This is a standard setup of a full-screen UIWebView inside of a view controller:

#import <UIKit/UIKit.h>

@interface WebViewController : UIViewController

@property (nonatomic, strong) NSURL *stationURL;

@end

Programmatically add a UIWebView to the WebViewController.m file with a private property to reference it:

#import "WebViewController.h"

@interface WebViewController ()
@property (nonatomic, strong) IBOutlet UIWebView *webView;
@end

@implementation WebViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    _webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
    _webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.view addSubview:_webView];

    NSURLRequest *request = [NSURLRequest requestWithURL:_stationURL];
    [_webView loadRequest:request];

    // Add a button to our navigation controller
    UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithTitle:@"Done"
                                      style:UIBarButtonItemStyleDone
                                      target:self
                                      action:@selector(doneButtonPressed:)];
    self.navigationItem.rightBarButtonItem = doneButton;
}

- (IBAction)doneButtonPressed:(id)sender
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

@end

Since we’ll eventually put this view controller inside a UINavigationController, we added the UIBarButtonItem to the navigation item for this view controller, at the end of -viewDidLoad.

Launching the modal

At the top of ViewController.m, import the new WebViewController.h header file:

#import "WebViewController.h"

Next, in the same file, update the -tapOnCalloutAccessoryControl:forAnnotation:onMap: method to instantiate the new web view controller, assign it to a navigation controller, then show that navigation controller modally.

- (void)tapOnCalloutAccessoryControl:(UIControl *)control forAnnotation:(RMAnnotation *)annotation onMap:(RMMapView *)map
{
    WebViewController *webVC = [[WebViewController alloc] init];
    webVC.stationURL = [NSURL URLWithString:annotation.userInfo[@"url"]];
    webVC.title = annotation.userInfo[@"title"];

    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:webVC];
    nav.modalPresentationStyle = UIModalTransitionStyleCoverVertical;
    [self presentViewController:nav animated:YES completion:nil];
}

Build and run, then tap the button on one of the callouts. You should see a modal view containing the web page for that station. Tapping Done in the navbar should close the modal view.

Screenshot

Adding line colors

Let’s add a bit of style to the left side of the callout and draw circles with the colors of the lines that each station supports. We’ll use Core Graphics to draw some circles in a UIView subclass on the fly.

Create a new file (like we did before) to your project called StationDotsView that is a subclass of UIView:

Screenshot

In the StationDotsView.h file, declare an -initWithLines: method in the @interface section that takes an NSSet argument:

#import <UIKit/UIKit.h>

@interface StationDotsView : UIView

- (instancetype)initWithLines:(NSSet *)lines;

@end

In the StationDotsView.m file, add a private property of type NSSet called lines. Create the -initWithLines: method as it appears below:

#import "StationDotsView.h"

@interface StationDotsView ()
@property (nonatomic, strong) NSSet *lines;
@end

@implementation StationDotsView

- (instancetype)initWithLines:(NSSet *)lines
{
    self = [super initWithFrame:CGRectMake(0, 0, 38, 25)];
    if (self)
    {
        self.backgroundColor = [UIColor clearColor];
        self.lines = lines;
    }
    return self;
}

@end

This -initWithLines: method is going to be the initializer for our UIView subclass. It takes a single argument, a NSSet of the lines for the station. We assign that argument to our lines instance variable.

Now that we can initialize the view with the lines, we need to override the -drawRect: method so we can do custom drawing with Core Graphics.

This method is a bit involved, but it is responsible for drawing six circles in the view, one for each colored line. As it draws each circle, if the station supports that color then the circle is filled in, otherwise it’s filled with a light gray. Add the following to StationDotsView.m:

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    // These are all the lines the station *could* support
    NSArray *lineColors = @[@"Blue", @"Green", @"Orange", @"Red", @"Silver", @"Yellow"];

    // These match the colors used by WMATA on their map, so let's use them to fill
    NSArray *fillColors = @[
                            [UIColor colorWithRed:0.01 green:0.56 blue:0.84 alpha:1], // Blue
                            [UIColor colorWithRed:0 green:0.68 blue:0.3 alpha:1], // Green
                            [UIColor colorWithRed:0.89 green:0.54 blue:0 alpha:1], // Orange
                            [UIColor colorWithRed:0.75 green:0.08 blue:0.22 alpha:1], // Red
                            [UIColor colorWithRed:0.64 green:0.65 blue:0.64 alpha:1], // Silver
                            [UIColor colorWithRed:0.99 green:0.85 blue:0.1 alpha:1] // Yellow
                            ];

    // Iterate over each of the lines and decide if we should draw a colored circle
    // (meaning this annotation supports that line) or a gray circle (meaning
    // that the station does *not* support that line

    for (int i=0; i<6; i++)
    {
        float left = i * 13 + 1;
        float top = 1;

        // The second row of dots needs adjustment
        if (i>=3) {
            left -= 39.0;
            top = 14;
        }

        // If the station does not support the current line, show a
        // light gray circle, otherwise fill with the line color

        UIColor *fillColor;

        if ([self.lines containsObject:lineColors[i]])
            fillColor = fillColors[i];
        else
            fillColor = [UIColor colorWithRed:0.83 green:0.83 blue:0.83 alpha:0.4];

        // Draw an ellipse (circle) inside of a positioned rect
        CGRect rectangle = CGRectMake(left, top, 10, 10);
        CGContextSetFillColorWithColor(ctx, fillColor.CGColor);
        CGContextFillEllipseInRect(ctx, rectangle);
    }

}

Finally, go back to our ViewController.m file and import our new StationDotsView.h header:

#import "StationDotsView.h"

In our -mapView:layerForAnnotation: delegate method, instantiate a StationDotsView object and add it as the left accessory of the callout just before the return statement:

NSSet *lines = annotation.userInfo[@"lines"];
StationDotsView *dots = [[StationDotsView alloc] initWithLines:lines];
marker.leftCalloutAccessoryView = dots;

return marker;

Build and run, then tap one of the markers. You should see colored circles in the callout representing the lines for that station.

Screenshot

Continue to part 4

We learned a lot in this part. We customized the markers for our annotations by providing a layer for the annotation, we added a button that launches a modal web page, and we created a custom view for our marker to show the line colors.

In the final section, we’ll learn how to filter the markers on the map by line color.

Additional questions? Ask our support team or learn more about How Mapbox Works.