intermediate
Swift or Objective-C
Get started with expressions using the Mapbox Maps SDK for iOS
Prerequisite
Familiarity with Xcode and either Swift or Objective-C, and completion of the Mapbox Maps SDK for iOS installation guide.

In this guide you’ll learn how to write expressions using the Mapbox Maps SDK for iOS to style custom data based on a data attribute and by zoom level.

adjust the circle radius based on zoom level

Getting started

This guide assumes you are familiar with Objective-C or Swift. Here are the resources you’ll need before getting started:

Download the data

What are expressions?

In the Mapbox Style Specification, the value for any layout property, paint property, or filter may be specified as an expression. Expressions define how one or more feature attribute values and/or the current zoom level are combined using logical, mathematical, string, or color operations to produce the appropriate style property value or filter decision.

The Mapbox Maps SDK for iOS allows you create expressions that follow the Mapbox Style Specification standard by using NSExpression. In this tutorial, you’ll create both property and zoom expressions to conditionally change a layer’s style.

A data expression is any expression defined using a reference to feature property data. Property expressions allow the appearance of a feature to change with its properties. They can be used to visually differentiate types of features within the same layer or create data visualizations.

Uses

There are countless ways to apply property expressions to your application, including:

  • Data-driven styling: Specify style rules based on one or more data attribute, such as coloring state polygon based on their population.
  • Arithmetic: Do arithmetic on source data, for example performing calculations to convert units on the fly.
  • Conditional logic: Use basic if-then logic, for example to decide exactly what text to display for a label based on which properties are available in the feature or even the length of the name.
  • String manipulation: Take control over label text with things like uppercase, lowercase, and title case transforms without having to modify, re-prepare and re-upload your data.

In this tutorial, you’ll learn how to use expressions to style Historic Preservation Commission landmarks in Minneapolis based on age and zoom level.

Set up a map

Initialize a map view

To begin building your data visualization, you first need to initialize a map view. Use the code below to create a simple map view that uses the Mapbox Light style and is centered on Minneapolis, Minnesota:

override func viewDidLoad() {

    let mapView = MGLMapView(frame: view.bounds)
    mapView.styleURL = MGLStyle.lightStyleURL
    mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

    mapView.setCenter(CLLocationCoordinate2D(latitude: 44.971, longitude: -93.261), zoomLevel: 10, animated: false)

    mapView.delegate = self
    view.addSubview(mapView)

}
- (void)viewDidLoad {
    [super viewDidLoad];
    
    MGLMapView *mapView = [[MGLMapView alloc] initWithFrame:self.view.bounds styleURL: [MGLStyle lightStyleURL]];

    mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

    [mapView setCenterCoordinate:CLLocationCoordinate2DMake(44.971, -93.261)
                       zoomLevel:10
                        animated:NO];

    [self.view addSubview:mapView];
    mapView.delegate = self;
    
}

The result will be a blank map using the Mapbox Light style, as shown below:

blank light map on an iOS device

Upload data

In this guide, you’ll use a vector tileset to display data in your application. You can create a vector tileset by uploading the CSV you downloaded earlier to Mapbox Studio:

  1. Visit the Tilesets page in Mapbox Studio.
  2. Click New tileset.
  3. Select the CSV you downloaded at the beginning of this tutorial and click Confirm.
  4. A popover will appear in the bottom right showing the progress of your upload.
  5. Once the upload has Succeeded, the tileset will be ready to use! Click on the name of the tileset in the popover, which will open the tileset information page.
  6. Take note of the map ID on the right side of the tileset information page. You will use the map ID to add this tileset to your map in the next step.

Add the source data to the map

To load the data onto the map, you’ll need to do two things:

  1. Add the vector tileset to the map dynamically as an MGLVectorTileSource to initially load the source data.
  2. Add a corresponding MGLCircleStyleLayer that references the above MGLVectorTileSource to display the data on the map.

The source data and style layer should be added after the map is finished loading, so the code to both should reside within the -mapView:didFinishLoadingStyle: delegate method.

func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
  
    let source = MGLVectorTileSource(identifier: "historical-places", configurationURL: URL(string: "mapbox://examples.5zzwbooj")!)
    style.addSource(source)

    let layer = MGLCircleStyleLayer(identifier: "landmarks", source: source)
    layer.sourceLayerIdentifier = "HPC_landmarks-b60kqn"
    layer.circleColor = NSExpression(forConstantValue: #colorLiteral(red: 0.67, green: 0.28, blue: 0.13, alpha: 1))    
    layer.circleOpacity = NSExpression(forConstantValue: 0.8)
    style.addLayer(layer)
  
}
- (void)mapView:(MGLMapView *)mapView didFinishLoadingStyle:(MGLStyle *)style {
  
  MGLSource *source = [[MGLVectorTileSource alloc] initWithIdentifier:@"historical-places" configurationURL:[NSURL URLWithString:@"mapbox://examples.5zzwbooj"]];
  [mapView.style addSource:source];

  MGLCircleStyleLayer *layer = [[MGLCircleStyleLayer alloc] initWithIdentifier:@"landmarks" source:source];
  layer.sourceLayerIdentifier = @"HPC_landmarks-b60kqn";
  layer.circleColor = [NSExpression expressionForConstantValue:[UIColor colorWithRed:0.67 green:0.28 blue:0.13 alpha:1.0]];
  layer.circleOpacity = [NSExpression expressionForConstantValue:@"0.8"];
  [mapView.style addLayer:layer];
  
}

When you run your application, you’ll see that the tileset has been added as a source to the map. You can now see circles marking where each landmark is with an assigned circle color and opacity.

adding a new circle layer to a map

Use an expression to calculate the age of each landmark

Next, you’ll write an expression to style the radius of each circle based on the age of each historic landmark. In this data file, provided by the City of Minneapolis’s open data portal, the age of the historic landmark is not provided, but the year of construction is provided.

Instead of manually editing your source data and re-uploading it, you can use arithmetic to calculate the age of each landmark based on the current year. Once the age of each landmark has been calculated, you can style the circleRadius paint property based on the calculated age of each landmark.

Start by calculating the age of the landmark and assigning it as the circle’s radius, such that older landmarks will have a larger circle radius.

func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
  
  ...
  layer.circleRadius = NSExpression(format: "2018 - Constructi")
  ...
  
}
- (void)mapView:(MGLMapView *)mapView didFinishLoadingStyle:(MGLStyle *)style {
  
  ...
  layer.circleRadius = [NSExpression expressionWithFormat:@"2018 - Constructi"]
  ...
  
}

The above expression subtracts the current year (2018) each feature’s date of construction. Constructi is the name of the key corresponds to the year each landmark was constructed. Run the application again to see the changes.

change the radius of each circle based on the age of each landmark

Wow! Those circles are quite large. This is because a radius is measured in screen points, whereas Constructi is measured in years. To make this look better, you’ll make some adjustments to the circle radius.

Adjust the circle radius

Since expressions can support mathematical operations, you can take the existing expression and divide the age by 10 to reduce the radius of each circle.

func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
  
  ...
  layer.circleRadius = NSExpression(format: "(2018 - Constructi) / 10")
  ...
  
}
- (void)mapView:(MGLMapView *)mapView didFinishLoadingStyle:(MGLStyle *)style {
  
  ...
  layer.circleRadius = [NSExpression expressionWithFormat:@"(2018 - Constructi) / 10"]
  ...
  
}
reduce the size of each circles radius

That looks better! Reducing the size of the circles makes this map much more legible at this zoom level. There are still some circles overlapping at this zoom level, so lets add a zoom expression to adjust the radius of the circles based on the zoom level the map is currently at.

Add a zoom expression

The circles are still overlapping quite a bit at the starting zoom level of 10, but they look good at higher zoom levels. You can use a zoom expression to address this issue, which will allow the the appearance of a layer to change with the map’s zoom level. Zoom expressions can be used to create the illusion of depth and control data density.

You will create a new linear interpolation expression to define a different circle radius between the following zoom levels:

Zoom level Circle radius
0-10 Calculated building age ÷ 30
13+ Calculated building age ÷ 10

By using a linear interpolation, the circle radius will remain the same in between categories. For example, at zoom level 18 the circle radius will still be the building age, divided by 10. There are other types of interpolations that would interpret this differently, which you can read about in more detail within the Maps SDK documentation.

Within your code, the linear interpolation expression will look like this:

func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {

    ...

    let zoomStops = [
      10: NSExpression(format: "(Constructi - 2018) / 30"),
      13: NSExpression(format: "(Constructi - 2018) / 10")
    ]

    layer.circleRadius = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", zoomStops)

   ...

}
func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
    
    ...

    NSDictionary *zoomStops = @{
                               @10: [NSExpression expressionWithFormat:@"(Constructi - 2018) / 30"],
                               @13: [NSExpression expressionWithFormat:@"(Constructi - 2018) / 10"]
    };

    layer.circleRadius = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:(Constructi, 'linear', nil, %@)", zoomStops];

    ...
    
}

Once you run your application again, you’ll notice that at the same zoom level, the circles appear more distinct.

adjust the circle radius based on zoom level

Finished product

Congratulations — you’ve styled custom data using expressions using the Mapbox Maps SDK for iOS!

The completed code for this tutorial is shown below.

override func viewDidLoad() {

let mapView = MGLMapView(frame: view.bounds)
mapView.styleURL = MGLStyle.lightStyleURL
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

mapView.setCenter(CLLocationCoordinate2D(latitude: 44.971, longitude: -93.261), zoomLevel: 10, animated: false)

mapView.delegate = self
view.addSubview(mapView)

}

func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
  
  let source = MGLVectorTileSource(identifier: "historical-places", configurationURL: URL(string: "mapbox://examples.5zzwbooj")!)

  style.addSource(source)

  let layer = MGLCircleStyleLayer(identifier: "landmarks", source: source)

  layer.sourceLayerIdentifier = "HPC_landmarks-b60kqn"

  layer.circleColor = NSExpression(forConstantValue: #colorLiteral(red: 0.67, green: 0.28, blue: 0.13, alpha: 1))

  layer.circleOpacity = NSExpression(forConstantValue: 0.8)

  let zoomStops = [
      10: NSExpression(format: "(2018 - Constructi) / 30"),
      13: NSExpression(format: "(2018 - Constructi) / 10")
  ]

  layer.circleRadius = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", zoomStops)

  style.addLayer(layer)
  
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MGLMapView *mapView = [[MGLMapView alloc] initWithFrame:self.view.bounds styleURL: [MGLStyle lightStyleURL]];

    mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

    [mapView setCenterCoordinate:CLLocationCoordinate2DMake(44.971, -93.261)
                       zoomLevel:10
                        animated:NO];

    [self.view addSubview:mapView];
    mapView.delegate = self;
    
}

func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
  
  MGLSource *source = [[MGLVectorTileSource alloc] initWithIdentifier:@"historical-places" configurationURL:[NSURL URLWithString:@"mapbox://examples.5zzwbooj"]];

  [mapView.style addSource:source];

  MGLCircleStyleLayer *layer = [[MGLCircleStyleLayer alloc] initWithIdentifier:@"landmarks" source:source];

  layer.sourceLayerIdentifier = @"HPC_landmarks-b60kqn";

  layer.circleColor = [NSExpression expressionForConstantValue:[UIColor colorWithRed:0.67 green:0.28 blue:0.13 alpha:1.0]];

  layer.circleOpacity = [NSExpression expressionForConstantValue:@"0.8"];

  NSDictionary *zoomStops = @{
      @10: [NSExpression expressionWithFormat:@"(2018 - Constructi) / 30"],
      @13: [NSExpression expressionWithFormat:@"(2018 - Constructi) / 10"]
  };

  layer.circleRadius = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:(Constructi, 'linear', nil, %@)", zoomStops];

  [mapView.style addLayer:layer];
  
}

Next steps