The iOS SDK Guide

The Meridian iOS SDK has all the tools you'll need to embed the Meridian Editor's maps, turn-by-turn navigation, indoor location awareness, and notifications into your own custom iOS app.

Once you've added maps, routes, placemarks, and campaigns to the Meridian Editor, you can use the Meridian SDK to integrate that content into your Meridian-powered iOS app.

Go here for the Meridian iOS SDK Reference documentation.

Click here to download the iOS SDK.

Add the SDK to Xcode

In order to simplify using the Meridian SDK library with your Xcode iOS project, we've bundled the SDK into a universal iOS framework.

The Meridian SDK must be compiled with Xcode 6 or higher and iOS 8 or higher.

You can add the SDK to your project using CocoaPods, an Objective-C and Swift dependency manager.

Add the SDK with CocoaPods

Complete these steps to add the Meridian SDK to your iOS Xcode project with CocoaPods.

  1. Install CocoaPods.

    $ gem install cocoapods

  2. CocoaPods uses a textfile named Podfile to list dependencies. In the Podfile, add:

    pod 'MeridianSDK'

  3. Install the dependences.

    $ pod install

  4. CocoaPods creates an .xcworkspace file. Make sure you use this to access your Xcode project.

Add SDK without CocoaPods

Complete these steps to add the Meridian SDK to your iOS Xcode project without CocoaPods.

Don't complete these steps if you're using CocoaPods.

  1. In your Xcode project, put the Meridian.framework folder into the Frameworks group.

  2. Put the Meridian.bundle folder into the Resources group.

  3. In Build Settings in the Other Linker Flags property, make sure the -all_load flag has been added. If not, then add it.

  4. Make sure the following system libraries are added to your project's dependencies:

    Foundation.framework
    UIKit.framework
    CoreLocation.framework
    CoreMotion.framework
    CoreBluetooth.framework
    SystemConfiguration.framework
    MobileCoreServices.framework
    libxml2.2.dylib
    libc++.1.tbd
    

Configure the SDK

In order to access the Meridian classes, in the relevant source files, add the line:

    #import <Meridian/Meridian.h>

The MeridianSamples project has an example of what your finished project structure should look like.

Before you start using the SDK's features, you'll need to configure the SDK for your project. To do this, create an instance of MRConfig and set it in your application:didFinishLaunchingWithOptions: method.

    MRConfig *config = [MRConfig new];
    [Meridian configure:config];

Implement Google Analytics

If you'd like to implement Google Analytics reporting for your app, add the following code to MSAppDelegate.m:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // configure the Meridian SDK
    MRConfig *config = [MRConfig new];
    config.googleAnalyticsTrackingCode = @"TRACKING_ID";
     // must be called once, in application:didFinishLaunching
    [Meridian configure:config];

Replace TRACKING_ID with your unique Google Analytics tracking ID.

Use Editor Keys

Use instances of MREditorKey to reference specific apps, maps, and placemarks that you've created in the Meridian Editor. Many of the Meridian SDK's classes require valid keys during initialization.

    // Creating a key representing your app in the Meridian Editor:
    MREditorKey *appKey = [MREditorKey keyWithIdentifier:@"5085468668573440"];

    // Creating a key representing a map in the MeridianEditor:
    MREditorKey *mapKey = [MREditorKey keyForMap:@"5708115733494264" app:@"5085468668573440"];

    // An alternate way to create a map key:
    MREditorKey *mapKey = [MREditorKey keyForMap:@"5708115733494264" app:appKey.identifier];

    // Creating a key representing a placemark:
    MREditorKey *placemarkKey = [MREditorKey keyForPlacemark:@"375634485771" map:mapKey];

When you create an instance of MREditorKey to represent a specific map, your app ID becomes the parent of the mapKey. Similarly, when you create a placemark key, the map ID becomes the parent of the [placemarkKey][placemarkKey].

    MREditorKey *placemarkKey = [MREditorKey keyForPlacemark:@"375634485771" map:someMapKey];

    MREditorKey *mapKey = placemarkKey.parent;
    NSLog(@"My map ID: %@", mapKey.identifier);

    MREditorKey *appKey = mapKey.parent;
    NSLog(@"My app ID: %@", appKey.identifier);

Display Maps

MRMapViewController includes a self-contained interface for maps and directions. This makes it unnecessary for you to create your own maps interface. Initialize MRMapViewController with a valid MREditorKey to get started.

    MREditorKey *mapKey = [MREditorKey keyForMap:@"5638404075159552" app:@"5468665088573440"]; // Meridian Portland Office
    MRMapViewController *mapController = [MRMapViewController new];
    mapController.mapView.mapKey = mapKey;

    [MRMapViewController][MRMapViewController] hosts an [MRMapView][MRMapView] and adopts the [MRMapViewDelegate][MRMapViewDelegate] protocol, which handles the basic tasks delegated by [MRMapView][MRMapView].

You can also use MRMapView directly in your own view controller, optionally implementing methods defined by the MRMapViewDelegate protocol so that your controller can respond to MRMapView events.

You can specify which map to display by setting the mapKey property of the view. Otherwise, the map marked as default in the Meridian Editor will be loaded.

    // In your view controller's `viewDidLoad` method

    MREditorKey *appKey = [MREditorKey keyWithIdentifier:@"5085468668573440"];
    MRMapView *mapView = [[MRMapView alloc] initWithFrame:self.view.bounds];

    // If you want to handle map view events
    mapView.delegate = self;

    // If you want to load a map other than the default one
    mapView.mapKey = [MREditorKey keyForMap:@"5708115733494264" app:appKey.identifier];

    [self.view addSubview:mapView];

Map Annotations

MRMapView will always annotate a map with its placemarks, provided you haven't set showsPlacemarks = NO. Similar to Apple's MapKit, the Meridian SDK defines the MRAnnotation protocol for map annotations and the MRMapViewDelegate protocol provides a callback to customize the appearance of map annotations.

You can adopt the MRAnnotation protocol in your own model objects to add them to your maps and you can provide your own views to represent instances of MRPlacemark.

Alternately, you can provide your own objects by overriding mapView:viewForAnnotation: and returning instances of your own subclass of MRAnnotationView.

In the following example, we have a custom view to represent placemarks named "Cafe":

    // In the custom view controller that adopts the MRMapViewDelegate protocol,
    // don't forget to set your controller as the delegate of the MRMapView.

    - (MRAnnotationView *)mapView:(MRMapView *)mapView viewForAnnotation:(id <MRAnnotation>)annotation {

        if ([annotation isKindOfClass:[MRPlacemark class]]) {
            if ([annotation.title isEqualToString:@"Cafe"]) {
                // Assume we've subclassed MRAnnotationView as CafeAnnotationView
                CafeAnnotationView *view = [mapView dequeueReusableAnnotationViewWithIdentifier:@"CafeMarker"];
                if (view == nil)
                    view = [[CafeAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"CafeMarker"];

                return view;
            }
        }

        // Returning nil will allow the default view to be used if the annotation is an instance of MRPlacemark
        return nil;
    }

Load Directions

MRMapViewController handles loading and presenting directions in response to user interactions.

When you create an instance of MRMapViewController, you can set the pendingDestination property to initiate directions to that MRPlacemark as soon as the map view is visible.

If you want to present directions modally, the SDK provides a convenient way to do so with an instance method on UIViewController(http://files.meridianapps.com/meridian-ios-sdk/docs-3.9.0/Categories/UIViewController+Directions.html).

    // Somewhere in your custom view controller
    // Assume myPlacemark is a valid MRPlacemark instance

    [self presentDirectionsToPlacemark:myPlacemark];

If you're using MRMapView directly in your own view controller, you'll need to handle loading directions manually. You can do this using MRDirectionsRequest. Specify the destination and starting point for the route using instances of MRMapItem.

Like MapKit's MKMapItem, MRMapItem contains information about a point on a map.

After configuring the request with the details of the desired route, you can use it to create an instance of MRDirections and then call calculateDirectionsWithCompletionHandler: to asynchronously calculate directions for the route.

Finally, provide a completion block to be executed when the request is complete, to handle the response and any errors.

    // Somewhere in your view controller
    // Assume myPlacemark is a valid MRPlacemark instance

    MRDirectionsRequest *request = [MRDirectionsRequest new];
    request.source = [MRDirectionsSource sourceWithCurrentLocation];
    request.destination = [MRDirectionsDestination destinationWithMapKey:placemark.key.parent withPoint:placemark.point];
    request.transportType = MRDirectionsTransportTypeAny;

    MRDirections *directions = [[MRDirections alloc] initWithRequest:request presentingView:self.view];
    [directions calculateDirectionsWithCompletionHandler:^(MRDirectionsResponse *response, NSError *error) {
        if (!error) {
            if (response.routes.count > 0) {

                // Normally only one route will be returned
                MRRoute *route = response.routes.firstObject;

                // If we have an MRMapView we can tell it to display this route
                [self.mapView setRoute:route animated:YES];

            } else {
                NSLog(@"Error loading directions: %@", error.localizedDescription);
            }
        }
    }];

In the example, MRDirectionsSource sourceWithCurrentLocation indicates the route should start at the user's current location. When creating the 'MRDirections' instance, the controller's view is passed as the presentingViewController: parameter.

A new route is calculated when the user's blue dot location is approximately 15 meters from the route.

When a valid current location is not available, this causes MRDirections to prompt the user to select a starting location with a modal search interface.

Send More Variables to Campaign Custom Endpoints

When using Campaign custom endpoints, you may want to send additional data to the custom endpoint.

Create a delegate for MRCampaignManager that implements campaignManager:userInfoForCampaign: to define variables that will be put into parameters and sent to the custom endpoint.

    - (NSDictionary *)campaignManager:(MRCampaignManager *)manager userInfoForCampaign:(NSString *)campaignIdentifier {
    return @{"custom_field_1": "custom_data"};
    }

Get User Location

MRLocationManager determines the user's location by gathering all the available location data for your Meridian-powered app. The MRLocationManagerDelegate protocol will make sure that you get callbacks as new locations are determined.

    // In a view controller that adopts the MRLocationManagerDelegate protocol
    // Assume we have properties called appKey and locationManager

    - (void)viewDidLoad {
        [super viewDidLoad];

        self.appKey = [MREditorKey keyWithIdentifier:@"5085468668573440"];
        self.locationManager = [[MRLocationManager alloc] initWithApp:self.appKey];
        self.locationManager.delegate = self;
    }

    // You may want to start getting location updates here
    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];

        [self.locationManager startUpdatingLocation];
    }

    // And you may want to stop getting location updates here
    - (void)viewWillDisappear:(BOOL)animated {
        [super viewWillDisappear:animated];

        [self.locationManager stopUpdatingLocation];
    }

    // Implementing MRLocationManagerDelegate methods
    - (void)locationManager:(MRLocationManager *)manager didUpdateToLocation:(MRLocation *)location {
        NSLog(@"New location: %@", location);
    }

    - (void)locationManager:(MRLocationManager *)manager didFailWithError:(NSError *)error {
        NSLog(@"Location error: %@", error);
    }

Location Sharing

Location sharing is a new Meridian feature that lets users share their location with each other. This feature is also available in our iOS and Android white-label apps.

Prerequisites

Location Sharing needs to be enabled by an admin in the Meridian Editor before you can use it in the SDK. If you'd like to enable Location Sharing, please email hpe-aruba-meridian-poc@hpe.com.

If you haven't already done so, you'll need to generate a valid token in the Editor. To generate one, in the sidebar menu click Permissions, and then click Application Tokens. Click Add Application + to generate a token for your app. You can name it anything.

When configuring the Meridian SDK, set the application token you generated in the Editor:

    MRConfig *config = [[MRConfig alloc] init];
    config.applicationToken = @"kd84hf83ck93k39dyi3nxo3mf94";
    [Meridian configure:config];

MRSharingSession

The MRSharingSession class is used to create, log in to, and delete accounts. It's also used to start and stop the sharing service and to receive location sharing updates.

Create an Account

    // Creates an account (password can be any string).
    self.password = [NSUUID UUID].UUIDString;
    [MRSharingSession createAccountWithPassword:self.password firstName:@"Morgan" lastName:@"Smith" appKey:myAppKey completion:^(MRSharingSession * _Nullable session, NSError * _Nullable error) {
        if (error) {
            NSLog(@"Something went wrong: %@", error.localizedDescription);
        } else {
            // Now there's a sharing session.
            NSLog(@"Got session: %@", session.key.identifier);
            // Keeps a reference to the session. Needed for most subsequent requests.
            self.session = session;
            // Makes self the delegate to get callbacks with fresh data.
            self.session.delegate = self;
        }
    }];

Logging In

    // Logging back in later.
    [MRSharingSession loginWithKey:self.session.key password:self.password appKey:myAppKey completion:^(MRSharingSession * _Nullable session, NSError * _Nullable error) {
        if (error) {
            NSLog(@"Something went wrong: %@", error.localizedDescription);
        } else {
            NSLog(@"Got session: %@", session.key.identifier);
        }
    }];

Start and Stop Location Sharing

    // Starting/stopping the service.
    - (void)startSharing {
        [self.session startListeningForUpdates];
        [self.session startPostingLocationUpdates];
    }
    - (void)stopSharing {
        [self.session stopListeningForUpdates];
        [self.session stopPostingLocationUpdates];
    }

    // Adopts the MRSharingSessionDelegate protocol to receive callbacks.
    - (void)sharingSession:(MRSharingSession *)session friendsDidUpdate:(NSArray<MRFriend *> *)friends {
        // The friends array has the latest location sharing data for each friend.
    }
    - (void)sharingSession:(MRSharingSession *)session didFailWithError:(NSError *)error {
        NSLog(@"Sharing session error: %@", error.localizedDescription);
    }

MRInvite

The MRInvite class is used to create, retrieve, and accept invitations.

Create and Retrieve Invitations

    // Creates invitations.
    [MRInvite createInviteWithSession:self.session completion:^(MRInvite * _Nullable invite, NSError * _Nullable error) {
        if (error) {
            NSLog(@"Something went wrong: %@", error.localizedDescription);
        } else {
            NSLog(@"Invitation URL: %@", invite.shareURL);
        }
    }];

    // Retrieves invitations.
    [MRInvite getAllInvitesWithSession:self.session completion:^(NSArray *invites, NSError *error){
        if (error) {
            NSLog(@"Something went wrong: %@", error.localizedDescription);
        } else {
            NSLog(@"Got %lu invitations", (unsigned long)invites.count);
        }
    }];

    // Accepts another user's invitation.
    NSURL *inviteURL = someValidShareURL;
    [MRInvite acceptInviteWithURL:someValidShareURL session:self.session completion:^(MRFriend * _Nullable friend, NSError * _Nullable error) {
        if (error) {
            NSLog(@"Something went wrong: %@", error.localizedDescription);
        } else {
            NSLog(@"Now sharing locations with %@", friend.firstName);
        }
    }];

MRFriend

Use the MRFriend class to load the details for a location sharing user, get a list of the user's friends, and more.

Get a User's Profile

    // Gets a user's profile.
    [MRFriend getFriendWithKey:self.session.key session:self.session completion:^(MRFriend * _Nullable friend, NSError * _Nullable error) {
        if (error) {
            NSLog(@"Something went wrong: %@", error.localizedDescription);
        } else {
            NSLog(@"Got data for %@", friend.firstName);
            // Updates a user's profile.
            [friend updateFirstName:@"Morgan" lastName:@"Smith" withSession:self.session completion:^(MRFriend * _Nullable friend, NSError * _Nullable error) {
                if (error) {
                    NSLog(@"failed to update profile: %@", error);
                } else {
                    NSLog(@"profile updated successfully");
                }
            }];
        }
    }

Get a List of Friends

    // Gets a list of a user's friends.
    [MRFriend getAllFriendsWithSession:self.session completion:^(NSArray<MRFriend *> * _Nullable friends, NSError * _Nullable error) {
        if (error) {
            NSLog(@"Something went wrong: %@", error.localizedDescription);
        } else {
            NSLog(@"Got %lu friends", (unsigned long)friends.count);
        }
    }];

You can use the MRLocalSearch, MRLocalSearchRequest, MRLocalSearchResponse, MRLocalSearchResult classes to find points of interest near the user's location. Combine these classes with MRLocationManager to provide a location and its nearby placemarks.

For best performance, limit local search results to 20 or less.

    - (void)findNearbyCafe {
        MRLocalSearchRequest *request = [MRLocalSearchRequest new];
        request.app = self.appKey;
        request.location = self.locationManager.location;
        request.transportType = MRDirectionsTransportTypeWalking;
        request.naturalLanguageQuery = @"Cafe";
        request.limit = 20;

        MRLocalSearch *search = [[MRLocalSearch alloc] initWithRequest:request];
        [search startWithCompletionHandler:^(MRLocalSearchResponse *response, NSError *error) {

            if (response) {
                for (MRLocalSearchResult *result in response.results)
                    NSLog(@"%@ is %f seconds away", result.placemark.name, result.expectedTravelTime);

            } else if (error) {
                NSLog(@"Error loading search results: %@", error.localizedDescription);
            }
        }];
    }

Asset Tracking

Asset Tracking is a feature that uses Aruba Tags hardware to track valuable locations on a map. You can use the SDK to get Tag location updates and render Tag locations on a map.

Show All Tags on a Map

To show all tags on a map, set the showsTags MRMapView variable to true:

func showTags(mapView: MRMapView) {
    mapView.showTags = true
}

Show a Specific Tag on a Map

To show a specific tag on a map, setup a predicate for that Tag's MAC address:

func showTag(mapView: MRMapView, tag: MRTag) {
    mapView.showsTags = true
    mapView.tagPredicate = NSPredicate.init(format: "mac LIKE %@", tag.mac)
}

Show a Tag from Search Results

To show a tag from search results, first use the Editor API to determine which map it’s on:

func showTagFromSearchResults(tag: MRTag, appID: String) {
    // MRTag returned via search doesn't contain location data.
    // Use the asset-beacons API to get more info.
    // Future SDK enhancements should make this explicit URL creation unnecessary.

    let tagEndpoint = "https://edit.meridianapps.com/api/locations/\(appID)/asset-beacons/\(tag.mac)"
    guard let url = URL(string: tagEndpoint) else {
        print("Error: cannot create URL")
        return
    }

    let task = URLSession.shared.dataTask(with: URLRequest(url: url)) {data, response, error in
        if let requestError = error {
            print("Tag request error: \(requestError.localizedDescription)")
            return
        }

        guard let tagResponse = response as? HTTPURLResponse, let tagData = data else {
            print("Error: did not receive tag response")
            return
        }

        if tagResponse.statusCode != 200 {
            print("Error: HTTP status code %li", tagResponse.statusCode)
            return
        }

        do {
            guard let json = try JSONSerialization.jsonObject(with: tagData, options: []) as? [String: Any] else {
                    print("error trying to convert data to JSON")
                    return
            }

            // Pull up the last map that a Tag was seen on.
            guard let calculationDictionary = json["calculations"] as? [String: Any],
                let defaultDictionary = calculationDictionary["default"] as? [String: Any],
                let locationDictionary = defaultDictionary["location"] as? [String: Any],
                let mapID = locationDictionary["map_id"] as? String
            else {
                print("Unable to retrieve location info from tag response")
                return
            }

            let controller = MRMapViewController()
            controller.mapView.mapKey = MREditorKey(forMap: mapID, app: appID)
            controller.mapView.showsTags = true
            controller.mapView.tagPredicate = NSPredicate.init(format: "mac LIKE %@", tag.mac)
            DispatchQueue.main.async {
                self.navigationController?.pushViewController(controller, animated: true)
            }
        } catch {
            print("error trying to convert data to JSON")
            return
        }
    }
    task.resume()
}

Removing the 20 Beacon Limit

With the release of the Meridian iOS SDK version 2.7.2, our developers have created a workaround to the iOS limit on region monitoring.

In previous versions of the SDK, locations were limited to a total of 20 Proximity Beacon push notification campaigns or regions. Once an iOS device detected 20 regions, it was unable to receive a push notification from any additional campaigns.

This new workaround uses a dynamic region monitoring strategy to listen to a continually rotating set of the Beacons nearest the device. Before reaching the 20 region limit, our new method unmonitors those regions that are farther away. As a result, an iOS device will never reach the region limit.

This new method uses a more active method for monitoring Proximity Beacons in the background. As a result, it's important to be mindful of your app's background usage in order to safeguard your users' device batteries.

Another advantage of this update is that you will no longer need to leave a Campaign's region in order to reset it.