Components

This following is an overview of the Favendo MapSDK Features.

Levelplans

If the SDK is used with GoogleMaps, it is possible to the use venue-specific indoor-maps already provided by GoogleMaps: GoogleMaps IndoorMaps

While there is little to no customization for this feature, it is possible to provide own level-plans - especially if the venue is not yet covered by Google. Currently there are three options:

  • provide a levelplan as UIImage

  • provide a URL-schema to display mapview tiles (e.g. for GoogleMaps)

  • provide a .mbtiles (SQLite) file that contains mapview tiles (e.g. for GoogleMaps)

See the objects LevelPlanChangeModel, LevelPlanChangeImageModel, LevelPlanChangeTilesUrlModel, LevelPlanChangeTilesFileModel for more information.

Markers and Clustering

Map Markers

The SDK contains automatic clustering of map markers. Out of the box, it comes with a wrapper-class which draws a marker with a text and a pin underneath. The text is aligned, line-wrapped, and resized automatically, as demonstrated in the screenshot. The screenshot displays special marker-wrappers for Backspin shops and facilities, where the venue-category is rendered in the pin, or the facility-icon is rendered in a "bubble".

It’s possible to write your own marker wrappers to create custom icons. If you need to, you can also create "native" markers for the given mapview-engine (e.g. Google Maps), though to keep the mapview-engines interchangeable, not all features might be available.

Positioning

If the Positioning mode is enabled, the mapview will display the position-marker on the corresponding level with an animated accuracy circle.

A mode to follow the user position is provided as well as map-autorotation for the compass-bearing (only for devices with a compass).

Requires the BackspinSDK framework or the creation of custom notifications.

Navigation

Turn By Turn

The MapSDK contains a navigation-mode in which multiple target locations can be defined. At each position-update, a route to the target locaton is calculated and displayed as a path automatically.

Includes turn-by-turn display with an arrow on the path indicating the next turn, as demonstrated in the screenshot. The drawn route starts at the nearest "snapped" navigation-path.

Requires the BackspinSDK framework.

Setup

Map Engine Selection

There are multiple versions of the Favendo MapSDK from which one can choose. Probably you are planning to only use one specific map-engine and stick to it, so you should choose the version for that engine.

We advise against using the global Favendo-iOS-Mapview for production / app-store apps, unless you are planning to use ALL map-engines and switch between them on runtime. For one, because the global version has the biggest size and contains code for map-engines that are then never used, and also because you still need to include ALL dependencies in order for submitting bitcode to Apple (since it "destroys" weak links and transforms them to required dependencies).

This is mostly relevant if you are NOT using the FavendoMapEngine but want to upload bitcode for your App to the App Store - in that case, you also need to embed the FavendoMapEngine.framework in your app, even though it is not used.

The global Favendo-iOS-Mapview is mostly recommended for testing and "internal" distribution beside the App Store.

Favendo iOS SDK Download page and select Map SDK with 3.12.8. In the last step you will have to options to choose from on how to embed the SDK:

CocoaPods (Easy Mode)

If using the source/dependency/library manager Cocoa Pods, the installation is straightforward. The page will generate a definition that can be pasted into your podfile. There is nothing else to do.

More stuff to do

Unless you choose the All (Debug) version of the Map SDK that links with all map-engines, the newer versions (>= 3.9.1) will have their dependencies setup already via Cocoapods.

For the All (Debug) version you need to link against Google Maps or embed the FavendoMapEngine.framework in your app, as you need a map-engine to work with.

Manually (Ninja Mode)

Download the zipped framework from the link generated at the Favendo iOS SDK Download page (Map SDK with 3.12.8) that matches your Xcode and Map-Engine requirements.

Embed the provided FavendoIndoorMapview.framework and depending on the used version, either link against GoogleMaps or embed the FavendoMapEngine.framework.

Dependencies

The Favendo MapSDK requires a map-engine to work. Currently this means either Google Maps or the own proprietary Favendo Map-Engine that works offline. You need to embed "FavendoMapEngine.framework" in the target app or link it with Google Maps, see

These are the following minimum version requirements:

  • GoogleMaps: 1.9.0

GoogleMaps has also been tested and verified to work with 2.1.1, 2.2.0, and 2.5.0

Resources Bundle

If GoogleMaps is used as map-engine, the GoogleMaps.bundle from the Google Maps framework needs to be included as resource in the target that uses the MapSDK.

Usage

If used with Google Maps, the first thing to do is to provide an API key, preferably within the application:didFinishLaunchingWithOptions: method. This can be done via a convenience wrapper of the SDK:

[FAVIndoorMapview provideMapsAPIKey:@"API_KEY"];

If you embed the FavendoMapEngine.framework in the app besides Google Maps, you need to set [FAVIndoorMapview setMapEngineToBeUsed:FAVIndoorMapEngineGoogleMaps]; for this to work, since otherwise the api-key might not be forwarded to GoogleMaps!

Simple Example

The FAVIndoorMapview main class wraps most of the mapview functionality, as seen in the following example:

FAVIndoorMapview *mapView = [[FAVIndoorMapview alloc] initWithFrame:self.view.frame];
mapView.translatesAutoresizingMaskIntoConstraints = false;

[self.view addSubview:mapView];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[mapView]|" options:0 metrics:nil views:@{@"mapView": mapView}]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[mapView]|" options:0 metrics:nil views:@{@"mapView": mapView}]];

// listen to mapview events (e.g. Google Maps events)
mapView.mapViewEventDelegate = self;
// listen to Favendo Mapview Events
mapView.favendoMapViewDelegate = self;

mapView.minZoomLevel = 15;
mapView.maxZoomLevel = 20;
// do not display any "native" map-engine elements
mapView.mapViewType = FAVIndoorMapSDKMapTypeNone;
// display a white background layer underneath the level-plan(s)
mapView.mapViewWrapper.backgroundLayerColor = @"FFFFFF";
// if the user scrolls outside the bounding box,
// scroll back to the center of the current level
mapView.maximumAllowedScrollingDistanceFromLevelPlanBounds = 0;

// center the user position, until the user scrolls or changes the level
mapView.mapViewScrollingState = FAVIndoorMapViewScrollingStateFollow;
// display a marker with the user's position
mapView.mapViewDisplayState = FAVIndoorMapViewDisplayStatePositioning;
// always switch to the current level, no "safety" intervals
mapView.immediateLevelSwitch = true;

// Set initial level
// ...
// see underneath

Since the MapSDK works with (indoor) levels, it is required to load a level after startup to use any indoor-functionality (markers, positioning, navigation).

Load (Initial) Level

The MapSDK functions in the context of levels. Unless the camera of the Map-engine instance (Access Map Engine) is manipulated directly, the only way to center the camera on a specific point on Earth is by setting a level with coordinate bounds.

If the BackspinSDK is used together with the MapSDK, there are convenience methods where just the level-number is needed and all other information are extracted from Backspin directly:

Using BackspinSDK

For the fastest start, you can use the following snippet:

// will load all levels from Backspin, display their level-numbers
// with the highest level-number at the top.
// will then load the level-plan and select the first/highest level
[mapView retrieveLevelsFromBackspinUsingLevelNames:false sortDescending:true withDefaultLevel:kFAVIndoorMapviewDefaultLevelNumber withCompletion:^{
    // after loading has completed, enable/show the default level-switcher
    mapView.enableLevelSwitcher = true;
}];

For more control, you can also trigger direct loading of a level:

- (BOOL) loadLevelWithLevelNumber:(NSInteger)levelNumber withFittingBehaviour:(FAVIndoorMapviewLevelPlanFitToBounds)fittingBehaviour withCompletion:(void (^ _Nullable)(void))completion;

or specify default camera-settings via a FAVIndoorMapviewLevelPlanChangeModel (e.g. initial zoom, fitting-behaviour, initial camera target)

- (BOOL) loadLevelWithLevelNumber:(NSInteger)levelNumber usingParametersFrom:(FAVIndoorMapviewLevelPlanChangeModel * _Nullable)levelChangeModel withCompletion:(void (^ _Nullable)(void))completion;

Manually

It is also possible to load a level without any data from Backspin by using the "Expert" API. For that one needs to import the private API:

#import <FavendoIndoorMapview/FAVIndoorMapview+InternalDebug.h>

import FavendoIndoorMapview.Private

To manually load a level, the MapSDK provides the FAVIndoorMapviewLevelPlanChangeModel, which sets a level either as UIImage or via "Google Maps" (image) tiles. There are 3 different instances that can be used: FAVIndoorMapviewLevelPlanChangeImageModel, FAVIndoorMapviewLevelPlanChangeTilesUrlModel, and FAVIndoorMapviewLevelPlanChangeTilesFileModel.

The following loads an "empty" level without levelplan-image, e.g. to

FAVIndoorMapviewLevelPlanChangeImageModel *levelPlanChangeImageModel = [FAVIndoorMapviewLevelPlanChangeImageModel new];

levelPlanChangeImageModel.levelNumber = itemLevelPlanModel.levelNumber;
levelPlanChangeImageModel.southWestLevelPlanCoordinate = CLLocationCoordinate2DMake(49, 10);
levelPlanChangeImageModel.northEastLevelPlanCoordinate = CLLocationCoordinate2DMake(50, 11);
// the first loaded level-plan should be displayed centered as whole, all others should keep the current zoom and scroll state.
levelPlanChangeImageModel.levelPlanFittingBehaviour = FAVIndoorMapviewLevelPlanFitToBoundsForFirstPlan;

// "workaround" if no levelplan is wanted, leave the image empty
levelPlanChangeImageModel.levelPlanImage = nil;
// if an image was given, it can be rotated. Use with caution, as the southWest/northEast bounds still have to fit!
levelPlanChangeImageModel.levelPlanRotation = 320;

[mapView setLevelPlanWithLevelPlanChangeModel:levelPlanChangeImageModel withCompletion:nil];

If the follow-mode is enabled (and positioning is active), the mapview-delegate method detectedLevelChangeToNewLevel: is called, after which a level-change-model should/can be provided as demonstrated above. The mapview itself does not change levels on its own, UNLESS the BackspinSDK is used - see the following section.

If the BackspinSDK is linked, the mapview CAN perform its own level-changes. detectedLevelChangeToNewLevel: will still be called, but only to inform, not as a call to action, since the mapview will handle level-changes on its own.

Level Switching

MapSDK Level Switcher

The mapview offers a built-in level-switcher, which is disabled by default. It offers the display of two states: If a navigation is active (via the BackspinSDK), it indicates the level of the current navigation-target via a flag-symbol, as well as the current level of the user-position via a little "arrow" - which is helpful if different from the current displayed/selected level.

If the BackspinSDK is linked, the levels-array can be filled conveniently via

- (void) retrieveLevelsFromBackspinUsingLevelNames:(BOOL)useLevelName sortDescending:(BOOL)sortDescending withDefaultLevel:(NSInteger)defaultLevel withCompletion:(void (^ _Nullable)(void))completion;

useLevelName is a flag to specify if the levelName attributes from the Backspin levels should be used or the levelNumber. The flag should only be set, if the level-names are short enough (3 characters at most), since the level-switcher will not expand. In doubt, you should keep using the level-numbers.

To enable it, simply call self.mapView.enableLevelSwitcher = true, after the levels have been filled.

Manually

It is also possible to manually fill the level-switcher with items:

FAVGenericSelectionItem *item = [FAVGenericSelectionItem itemWithText:@"2"];
item.userInfo = SomeCustomModel;
self.mapView.levels = @[item];
self.mapView.enableLevelSwitcher = true;

The itemText will be displayed without modification, so it should only contain 2-3 characters at maximum, like "-2A", "1B". Since a level-number is required as integer, a delegate method returning the integer value is provided.

E.g. item.itemText = @"2";

- (NSInteger) levelNumberForLevelSwitcherItem:(FAVGenericSelectionItem *)levelSwitcherItem {
    return levelSwitcherItem.itemText.integerValue;
}

The delegate method does not need to be implemented, if the String value in itemText represents the level-number. The MapSDK will then convert it via NSString.integerValue. If the String-value has a different number than the actual level-number or does not represent a number (e.g. "GF" for ground floor instead of 1), then the number should be returned.

Also optionally, a generic switcher view for the navigation bar is provided.

This switcher view can only be used with a UINavigationController.

NSMutableArray *allLevelSelectionItems = [NSMutableArray new];
// indoorLevelsForNavigation retrieved via the BackspinSDK
for(BSSDKLevelPlanModel *oneLevelPlanModel in indoorLevelsForNavigation) {
    FAVGenericSelectionItem *item = [FAVGenericSelectionItem itemWithText:@"2"];
    item.userInfo = oneLevelPlanModel;
    item.itemText = oneLevelPlanModel.levelName;

    [allLevelSelectionItems addObject:item];
}

kFAVIndoorMapViewAppearanceTableViewFontSize = 14.0; // minor customization
FAVGenericNavigationBarSwitcherView *navBarSwitcher = [[FAVGenericNavigationBarSwitcherView alloc] initWithFrame:CGRectZero andItemsToBeOfferedForSelection:allLevelSelectionItems andActiveItem:allLevelSelectionItems.firstObject];
navBarSwitcher.selectionDelegate = self;
[navBarSwitcher addToNavigationBarOfViewController:self]; // "self" is a UIViewController!

This will add a level-switcher to the navigation-bar and trigger a modal tableview-popup on tap:

Level Switching

The popup contains all items. The colors can be customized and an icon can be defined to be displayed at the left of each item, just like in a regular UITableViewCell.

Level Switching

See also the section about the generic selection popup.

Display Map Markers

The mapview supports displaying and clustering map-markers per default, it comes with a powerfull out-of-the-box model:

// trigger a refresh of map markers at the current level
[self.mapView updateMarkersForCurrentLevel];

In the minimum version, only one delegate method needs to be implemented that returns the map-markers. In the following example, all Venue-models are loaded via the BackspinSDK for the current level:

- (NSDictionary *) mapMarkerWrappersForLevel:(NSInteger)levelNumber {
    NSMutableArray *createdVenueMapMarkers = [NSMutableArray new];

    FAVIndoorMapTextMarkerAppearance *appearance = [FAVIndoorMapTextMarkerAppearance new];
    appearance.markerColor = @"FF0000"; // the pin should be red
    appearance.fontColor = @"0000FF"; // the text should be blue

    NSArray *venueModels = [BSSDKModelStore modelsForType:[BSGenericModelSQLite modelTypeVenue] withFilters:nil andRootScopeId:kBSSDKModelStoreUseCurrentRootScopeId];
    for(BSGenericModelSQLite *oneVenueModel in venueModels) {
        for(BSGenericModelSQLite *venueLocationModel in [oneVenueModel venueLocations]) {
            if([venueLocationModel integerForName:@"levelNumber"] == levelNumber) {

                FAVIndoorMapMarkerPinWithTextWrapper *defaultWrapper = [FAVIndoorMapMarkerPinWithTextWrapper new];
                defaultWrapper.markerPosition = CLLocationCoordinate2DMake( [([venueLocationModel venueLocationCenter][kFAVLatitude]) doubleValue], [([venueLocationModel venueLocationCenter][kFAVLongitude]) doubleValue]);
                defaultWrapper.markerLevelNumber = self.mapView.currentLevelNumber;
                defaultWrapper.markerPinImage = [UIImage imageNamed:@"mapMarkerPin"];
                defaultWrapper.markerText = [oneVenueModel venueName];

                defaultWrapper.markerAppearance = appearance;

                // user-info to identify this marker later on, set the Backspin model
                defaultWrapper.userInfo = venueLocationModel;

                [createdVenueMapMarkers addObject:defaultWrapper];
            }
        }
    }
}

return @{@"venues": createdVenueMapMarkers};
}

For performance reasons, it is recommended to save the set of created markers if it does not change. Note that you do not need to perform clustering in this method, this is done on the whole set within the mapview.

Depending on the zoom-level and orientation, the clustering will have different results. Clustering is activated by default. It can be disabled for single map markers, or globally (e.g. for better performance on slower devices).

// exclude single marker from clustering
FAVIndoorMapMarkerPinWithTextWrapper *defaultWrapper = [FAVIndoorMapMarkerPinWithTextWrapper new];
defaultWrapper.shouldBeExcludedFromClustering = true;

// disable clustering globally
self.mapView.enableMarkerClustering = false;

Backspin Venue Markers

Default Marker Icon

The MapSDK offers two convenience map-marker wrappers for Backspin venues: One for "shops" and one for "facilities" - as can be seen in the screenshot at Map Markers. For "shops", the first found venue-category logo will be displayed in the pin - if no venue-category is linked or it has no logo then a default icon is used as can be seen for the "Mensa" venue.

the loading of the venue-category logos is done asynchronously, so as long as they are loaded, the shown default-icon is displayed.

There is also a convenience method to create these marker-wrappers from a list of venue-models:

NSArray *venueModels = [BSSDKModelStore modelsForType:[BSGenericModelSQLite modelTypeVenue] withFilters:nil andRootScopeId:kBSSDKModelStoreUseCurrentRootScopeId];

// contains arrays of map-marker-wrappers grouped by level
NSDictionary *venuesByLevel = [FAVIndoorMapview venueMarkersForVenues:venueModels useFacilityWrappers:true];

// both Backspin venue wrappers derive from FAVIndoorMapMarkerPinWithTextWrapper
// now add all marker-wrappers at level 0 to the map
// CAUTION: you should make sure, level 0 actually exists and has markers
for(FAVIndoorMapMarkerPinWithTextWrapper *venueMarkerWrapper in venuesByLevel[@(0)]) {
    [self.mapView addPermanentMarker:venueMarkerWrapper];
}

Marker Info View

The backspin-markers also allow for displaying a marker detail-view at the bottom of the screen. If a marker is selected, the marker-delegate-method shouldScrollToMarkerThatHasBeenTappedForWrapper: will be called. If it is a venue-marker (BackspinVenueWrapper or BackspinFacilityWrapper), the marker-info-view can be filled with information about the selected venue(location) to display more information:

// in FAVIndoorMapsMapMarkerDataSource
- (BOOL) shouldScrollToMarkerThatHasBeenTappedForWrapper:(id<FAVIndoorMapsMarkerWrapper>)markerWrapper {
    if([markerWrapper isKindOfClass[FAVIndoorMapMarkerBackspinVenueWrapper class]]) {
        FAVIndoorMapMarkerBackspinVenueWrapper *venueWrapper = (FAVIndoorMapMarkerBackspinVenueWrapper)markerWrapper;
        self.mapView.markerInfoBottomView.venueModel = venueWrapper.venueModel;
        self.mapView.markerInfoBottomView.venueLocationModel = venueWrapper.venueLocationModel;

        // slide in the info view
        [self.mapView toggleMarkerInfoBottomView:true];
    }

    // center the tapped marker
    return true;
}

// in FAVIndoorMapviewDelegate
- (void) mapMarkerInfoViewReceivedTap {
    // used tapped the info-view, perform an appropriate action here
    // display a detail-view for the venue(location)?

    BSGenericModelSQLite *venueModel = self.mapView.markerInfoBottomView.venueModel;
    // ....
}

If positioning is enabled and a navigation-graph has been uploaded to Backspin, the info-view will contain live-distances to the selected venue-location.

Marker Convenience Methods

There are some shortcuts to creating markers. First, if just a simple marker should be displayed on the map (e.g. for testing purposes), there is the FAVIndoorMapMarkerSimple marker wrapper, that just takes an icon or color.

Furthermore, such a marker-wrapper can be added to the mapview by simply calling addPermanentMarker:

FAVIndoorMapMarkerSimple *simpleMarker = [FAVIndoorMapMarkerSimple new];
simpleMarker.markerIcon = [UIImage imageNamed:@"myMarker"];
simpleMarker.markerPosition = CLLocationCoordinate2DMake(49, 10);
simpleMarker.markerLevelNumber = 3;
[mapView addPermanentMarker:simpleMarker];

Markers that have been added like this will be displayed on the map, until the Favendo MapSDK instance is destroyed or they are removed via removePermanentMarker: You should keep a reference to the marker-wrapper to be able to remove it later.

Markers that are added like this will also partake in the clustering - unless shouldBeExcludedFromClustering is set.

It is also possible to manually create GMSMarker instances that can be added to Google Maps (Google Maps: Draw Markers), but these markers are excluded from clustering. Since during level-changes the Google Maps GMSMapView instance is destroyed and newly initialized, any custom added markers will be lost as well.

Positioning

If linked with the BackspinSDK, the position will be displayed automatically if the mapview display-state Positioning or Navigation is enabled. If not, the position-marker can still be used if a notification is published as such in the NSNotificationCenter:

  • name @"kFAVIndoorLocationCalculatedNotification"

  • user-info dictionary key location has a CLLocation

    • level-number encoded as altitude

    • accuracy (for accuracy circle) as horizontalAccuracy

mapView.mapViewDisplayState = FAVIndoorMapViewDisplayStatePositioning;

// publish a location update at 49,10 for level 1
CLLocation *indoorLocation = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(49, 10) altitude:1 horizontalAccuracy:0 verticalAccuracy:0 timestamp:[NSDate date]];
[[NSNotificationCenter defaultCenter] postNotificationName:@"kFAVIndoorLocationCalculatedNotification" object:nil userInfo:@{@"location": indoorLocation}];

Navigation

If used together with the BackspinSDK, the MapSDK has a built-in navigation-feature which is enabled via incoming position-updates, if the mapview display-state has been set to Navigation.

A set of target locations can be defined - for example entrance-points for a venue - from which the location with the shortest path will be targeted.

As with the Positioning the level-number should be encoded as altitude in the CLLocation objects.

@property (nonatomic) NSArray<CLLocation *> *possibleNavigationTargetLocations;

Having set this property while keeping the mapViewDisplayState on Navigation, a navigation-path will be calculated and drawn for the current displayed level once the next position-update is received.

Turn By Turn

The interval (in seconds) at which a new path is calculated can be set via navigationPathUpdateInterval. It is recommended to test out a reasonable interval depending on the use-case - it is most often not needed to calculate/display a new path more often than every five seconds, since the user does not move as fast.

BackspinSDK

If linked with the BackspinSDK, there is also a convenience setter navigationTargetVenueLocation, which will fill possibleNavigationTargetLocations with the entrances of the venue-location model (or, if no entrance is defined, the center-coordinate of the venue-location).

Also, if enabled via displayNavigationDisplay and displayTurnByTurnDisplay flags on the mapview object, at the top and bottom of the screen info-views will slide in during an active navigation that display additional information like time to destination and total remaining distance, including turn-by-turn directions for the next turn, as can be seen in the demo screenshot.

The views are accessible via turnByTurnTopView and navigationBottomView to customise the labels or background-colors. You should not change any layout-constraints of these views, as it might interfere with MapSDK-functions.

Asset Tracking

The map also offers support for Backspin asset-tracking to display and update asset-markers, including navigation.

To enable, first make sure that you have a valid token set in the BackspinSDK. After that by setting the flag watchAssetPositions on the FAVIndoorMapview instance the auto-updates of asset-markers can be enabled. A selected marker can be passed to the bottom-view to display information and offer to start a navigation to it:

func shouldScrollToMarkerThatHasBeenTapped(for markerWrapper: FAVIndoorMapsMarkerWrapper) -> Bool {
	switch markerWrapper {
		case let assetMarker as FAVIndoorMapMarkerBackspinAssetWrapper:
			_ = mapView.markerInfoBottomView.update?(withMarker: assetMarker)
            mapView.toggleMarkerInfoBottomView(true)
			break
		default:
			break
	}

    return true
}

To react to updates, you can register to the delegate-callback updatedAssetMarkerPositions, which will be called each time any asset-marker has been updated.

Expiration Handling

Furthermore, the map supports automatic asset-position-expiration handling. If an asset has not moved for a specified time, it can be removed or grayed out.

There are two properties to manipulate that behaviour:

assetMarkerExpirationInterval defines the time until an expiration-handling should take place.

assetMarkerExpirationHandling defines if the marker should be hidden or grayed out.

Map Rotation Sources

There are several sources that can be used for map- and position-marker-rotation:

  • compass
    this is the default source, which uses the device’s compass. It is also possible to provide external values to be used, if there is a datasource that provides updates. Usually this is what you want in a building/venue that is static and has not much interference with the compass.

  • navigation
    this source is additional to the other sources and only affects the map-orientation. If a navigation is started via BackspinSDK, if this source is active, the map will align with the first segment of the navigation. The position-marker can still rotate in another/the "real" direction, if other sources are enabled and usable.

  • location
    the last source will take the course attribute of the fed CLLocation objects and align the position-marker with it. If this source is used, you should also use mapMarkerRotationDuration to set an animation speed, if the position-updates are not delivered more than 15 times per second - the animation-speed makes sure, that the position-marker is rotated smoothly and there are no sudden "jump".

The following example will set the rotation-sources to navigation and location and sets a rotation-animation duration of 500ms.

FAVIndoorMapview *mapView;

// ...
// setup the mapview
// ...

FAVIndoorMapRotationSource rotationSources = FAVIndoorMapRotationSourceNavigation | FAVIndoorMapRotationSourceLocation;
mapView.mapRotationSource = rotationSources;
mapView.mapMarkerRotationDuration = 0.5;

Access Map Engine

To access the underlying map engine (e.g. GoogleMaps), the wrapper-object can be accessed via mapViewWrapper, which is an API-wrapper for the concrete map-engine instance. It contains a mapView UIView object. It can be casted to GMSMapView, if Google Maps is linked.

Listening to map engine events, the mapViewEventDelegate on the mapview can be set, so events are forwarded (unless captured internally already, e.g. marker-events).

For Google Maps specificially, there is the googleMapsMapViewDelegate as GMSMapViewDelegate.

However, this might interfere with some features in the Favendo MapSDK, so use with caution!

Drawing on map view

To draw custom objects (e.g. circles, polygons, polylines), map-engine wrappers can be created and modified via FAVIndoorMapSDKWrapperHelper.

Currently 4 map-object types are supported (besides floorplan-layers):

+ (id<FAVIndoorMapSDKMarkerWrapper>) createMarker;
+ (id<FAVIndoorMapSDKCircleWrapper>) createCircle;
+ (id<FAVIndoorMapSDKPolylineWrapper>) createPolyline;
+ (id<FAVIndoorMapSDKPolygonWrapper>) createPolygon;

Look into these wrapper-objects in FAVIndoorMapSDKWrapper.h. After creation, they can be added to the mapview by setting their mapView property to the FAVIndoorMapview.mapViewWrapper object.

Generic Selection Popup

While being designed to be used as a level-switcher, the popup can be utilized in multiple ways.

It consists of two parts that complement each other:

  • Navigation Bar Switcher
    A view for the navigation-bar in a UINavigationController. Will automatically add itself as UINavigationItem titleView. On tap, it will open the popup modally.

  • Selection Popup
    A modal popup that contains a UITableView, offering a set of items which can be selected. The current active item has a checkmark indicator at the left.

A simple usage example for the navigation bar switcher can be found at Navigation Bar Switcher.

The popup can be used as stand-alone without the navigation-bar switcher. It is customizable/themable.

It requires a sorted list of selection items and a delegate to handle selection events. This is the complete setup:

NSMutableArray *allLevelSelectionItems = [NSMutableArray new];
// indoorLevelsForNavigation retrieved via the BackspinSDK
for(BSSDKLevelPlanModel *levelPlanModel in indoorLevelsForNavigation) {

    FAVGenericSelectionItem *item = [FAVGenericSelectionItem itemWithText:levelPlanModel.levelName];
    item.userInfo = levelPlanModel;

    [allLevelSelectionItems addObject:item];
}

FAVGenericSelectionPopup *popup = [FAVGenericSelectionPopup new];
popup.itemsToBeOfferedForSelection = allLevelSelectionItems;
popup.currentSelectedItemIndex = 2;

popup.closeButtonText = @"Cancel";
popup.titleForPopup = @"My Popup";

[popup presentPopup];

There are several ways to customize the opening table-view controller, including using your own UITableViewCell instances. For minimum functionality however, only one method needs to be implemented, see more in the FAVGenericSelectionDelegate (in FAVGenericNavigationBarSwitcherView.h)

- (void) genericSelectionPopupDidSelectItemAtIndex:(NSInteger)selectedItemIndex withItem:(FAVGenericSelectionItem *)item {
    // handle the selection of this item
}

A convenient way to add asynchronous image loading for the icons is provided with the following delegate method:

- (void) lazyLoadingIconForItem:(FAVGenericSelectionItem *)item withCallback:(FAVIndoorMapviewGenericSelectionPopupLazyIconLoadingCompletion)lazyLoadingCompletionCallback

Once the UIImage has been loaded, the completion-block can be called with the image and it will be set in the table-view-cell.

The navigation bar switcher uses the popup and forwards its selection-items to the popup. It only needs to be added to a view-controller that has a UINavigationController and everything will be wired up automatically:

FAVGenericNavigationBarSwitcherView *navBarSwitcher;
// ....
[navBarSwitcher addToNavigationBarOfViewController:self]; // "self" is a UIViewController!

All delegate methods are still called and should be implemented according to necessity.

Take a look at the FAVGenericSelectionDelegate for further event-handling.

Customisation

There are various ways to customise the MapSDK appearance and parts of it. In general there are global variables that can be set on runtime, defined in FAVIndoorMapviewAppearance.h. The colors that can be defined are represented by strings in Hex-Format (as known from HTML). So the color red would be @"FF0000".

The category UIColor (FAVIndoorMapViewHex) allows converting this string to a UIColor.

If any of the following (color) properties is nil, a fallback might be triggered to use a more general color. If a color property should be using a default value, use the defined constant kFAVIndoorMapviewAppearanceColorNone, which solves the ambiguity of nil

Colors

  • kFAVIndoorMapViewAppearanceGeneralColor
    will be used as global color and fallback if no color is defined (and kFAVIndoorMapviewAppearanceColorNone isn’t used)

  • kFAVIndoorMapViewAppearancePositionMarkerColor
    The position-marker will be tinted in this color.

  • kFAVIndoorMapViewAppearancePositionAccuracyCircleColor
    A semi-transparent circle is displayed around the position-marker with this color, indicating the accuracy of the position in meters. Side note: Since the circle has a radius in meters, its dimension is dependent on the zoom-level of the map, while the position-marker itself keeps the same size. Set to kFAVIndoorMapviewAppearanceColorNone if you do not want an accuracy circle to be displayed.

Position Marker

Depending if the device has a compass or not (and it is enabled in the mapview), the provided marker contains a direction indicator, with an animated accuracy-circle:

Direction Arrow Direction Arrow

While the tint-color for the position-marker and the underlying accuracy-circle can be (separately) adapted, it is also possible to provide own marker-icons via mapview delegate. Whether or not the device has a compass is indicated by the parameter:

- (UIImage *) mapViewPositionMarkerImageWithDirectionIndicator:(BOOL)markerWithDirectionIndicator;
- (CGPoint) mapViewPositionMarkerGroundAnchor;

If the provided image is not be tinted, use kFAVIndoorMapviewAppearanceColorNone for the global variable kFAVIndoorMapViewAppearancePositionMarkerColor

Map Markers

Map Markers

Besides the position-markers, there are "ordinary" map-markers that can be customized as well. The most simple way is to use the built-in map-marker class (a pin with a text) and adapt its appearance via a FAVIndoorMapTextMarkerAppearance instance.

Here the default font-size, font-name, and color for the map-marker-text can be defined, as well as the size and color for the pin-image. A custom pin image can be set via markerPinImage in the FAVIndoorMapMarkerPinWithTextWrapper instance.

Shown in the image are two special markers which are derived from FAVIndoorMapMarkerPinWithTextWrapper - markers vor Backspin venues and Backspin facilities. These only work if the mapview is linked with the BackspinSDK.

Even more customised markers

For more customisation, the FAVIndoorMapsMarkerWrapper can be implemented, which (possibly asynchronously) returns a UIImage.

For each marker, the FAVIndoorMapsMarkerWrapper instance is set as userInfo. So if a Google Maps delegate method is implemented, you may access your wrapper via the userInfo field - unless it is a custom set marker or the position-marker.

Whitelabel Facility Markers

Here is a simplified example from the Backspin Whitelabel app for custom markers that represent facilities (toilets, stairs, elevators). The background is a custom black "marker-bubble" anchored at the bottom center, on which transparent facility-icons are drawn.

The implementation derives from FAVIndoorMapMarkerPinWithTextWrapper and overrides the setImageForMarker: method.

@interface FacilityMapMarkerWrapper: FAVIndoorMapMarkerPinWithTextWrapper
@property FacilityModel *facilityModel
@end

@implementation FacilityMapMarkerWrapper
- (void) setImageForMarker:(GMSMarker *)marker {
    UIImage *backgroundIcon = [UIImage imageNamed:@"facilityMarkerBackground"];

    // get the icon for the facility
    [self assembleFacilityMarker:marker icon:self.facilityModel.image backgroundIcon:backgroundIcon];

    self.markerDimensions = placeholderIcon.size;
    self.markerAnchor = CGPointMake(0.5, 1.0);
}

- (void)assembleFacilityMarker:(GMSMarker *)targetMarker icon:(UIImage *)icon backgroundIcon:(UIImage *)backgroundIcon {
    if (targetMarker == nil) {
        // target marker no longer displayed on map (e.g. clustering)
        return;
    }

    CGPoint backgroundIconDrawingPoint = CGPointZero;
    CGPoint iconDrawingPoint = CGPointZero;

    // ...
    // perform resizing of images so the icon fits to background icon
    // use UIGraphics bitmap context, for example
    // ...

    UIGraphicsBeginImageContextWithOptions(backgroundIcon.size, NO, backgroundIcon.scale);

    [backgroundIcon drawAtPoint:backgroundIconDrawingPoint];
    [icon drawAtPoint:iconDrawingPoint];

    UIImage *markerIcon = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    [targetMarker setIcon:markerIcon];
}
@end

This is what the FAVIndoorMapMarkerBackspinFacilityWrapper does, essentially.

Navigation Paths

If the navigation feature is enabled (i.e. the BackspinSDK is used), the appearance of the path can be customized as well.

  • kFAVIndoorMapViewAppearanceNavigationPathStrokeWith

  • kFAVIndoorMapViewAppearanceNavigationPathColor

  • kFAVIndoorMapViewAppearanceNavigationTurnByTurnArrowColor

  • kFAVIndoorMapViewAppearanceNavigationLevelChangeIconColor

The MapSDK comes with a default arrow used for turn-by-turn displays:

Direction Arrow

This arrow will be tinted according to the kFAVIndoorMapViewAppearanceNavigationTurnByTurnArrowColor variable. If another arrow should be used, its icon can be returned via the MapSDK delegate method:

- (UIImage * _Nullable) mapViewNavigationTurnByTurnDirectionsArrow

This arrow will be tinted as well unless the variable above is set to kFAVIndoorMapviewAppearanceColorNone.

Level Switcher

For one, an own level-switcher can be implemented that is informed about events by implementing the FAVIndoorMapviewLevelSwitcher protocol and setting the levelSwitcher property on the mapview.

However, for simple customisations, the provided level-switcher has some tweaks on its own:

Icon Position

The system-icons (a triangle to indicate the current user’s level as well as a target-flag to indicate the navigation-target level) can be displayed left or right (default is left) of the level-number. Furthermore, for each level a custom icon can be added on the left or right, which can be prioritised to be displayed instead of the system icons:

// system icons should be shown on the right side
mapView.systemIconPosition = FAVIndoorMapviewLevelSwitcherIconPositionRight

UIImage *icon = [UIImage imageNamed:@"customIcon"];

// set a custom icon for level 4 and 5 on the right side
// should be shown instead of system icons, if there might be any
[mapView setIcon:icon withLevelNumber: 4 position: FAVIndoorMapviewLevelSwitcherIconPositionRight prioritisingSystemIcon: true];
[mapView setIcon:icon withLevelNumber: 5 position: FAVIndoorMapviewLevelSwitcherIconPositionRight prioritisingSystemIcon: true];

// add and remove an icon on the left side
[mapView setIcon:icon withLevelNumber: 4 position: FAVIndoorMapviewLevelSwitcherIconPositionLeft prioritisingSystemIcon: true];
[mapView removeIconWithLevelNumber: 4 position: FAVIndoorMapviewLevelSwitcherIconPositionLeft];

Generic Selection Popup

There are six constants that specify the global appearance of the popup:

  • kFAVIndoorMapViewAppearanceNavigationBarColor

  • kFAVIndoorMapViewAppearanceNavigationBarTextColor

  • kFAVIndoorMapViewAppearanceTableViewFontName

  • kFAVIndoorMapViewAppearanceTableViewFontSize

  • kFAVIndoorMapViewAppearanceSelectionPopupItemColor

  • kFAVIndoorMapViewAppearanceSelectionPopupBackgroundColor

If needed, single popups can be uniquely customized via the local properties:

  • tableViewBackgroundColor

  • tableViewItemColor
    This color will be used as tint for the tableview cell. Will only have an affect if no custom tableview cell is returned from the delegate (see underneath)

TableView Customizing

It is possible to use own UITableViewCell (subclass) instances for the popup, which can be returned via the delegate method

- (UITableViewCell *) genericSelectionPopupCustomTableViewCellForItem:(FAVGenericSelectionItem *)item atIndexPath:(NSIndexPath *)indexPath inTableView:(UITableView *)selectionTableView

To make things even easier, the UITableView dequeueReusableCellWithIdentifier:forIndexPath: can also be used, if a nib or class is registered inside the delegate method:

- (void) registerCustomTableViewCellForTableView:(UITableView *)selectionTableView

Localization

By default, the MapSDK has localized outfor for German and English (e.g. for turn-by-turn directions). The provided keys and translations can be seen in the FavendoIndoorMapviewBundle.bundle/en.lproj/Localizable.strings.

If translations for another language should be provided, or the existing strings should be customized, you can implement the delegate-method to provide own strings:

The FAVIndoorMapviewDelegate protocol defines a method translateTurnByTurnString for which you can return a string of your choosing for the given key.

Here is the complete extract of the English Localizable.strings file:

"routing_bearing_left" = "left";
"routing_bearing_right" = "right";


"routing_turnXinYdistance" = "turn %direction% in %distance% %unit%";
"routing_bearXinYdistance" = "bear %direction% for %distance% %unit%";
"routing_headXdistancestraight" = "%distance% %unit% straight";
"routing_changeintoXinYdistance" = "change to %direction% in %distance% %unit%";
"routing_destinationInYdistance" = "destination %distance% %unit% ahead";

"routing_nodistance_Xturnahead" = "%direction% turn ahead";
"routing_nodistance_turnXnow" = "turn %direction% now";
"routing_nodistance_bearX" = "bear %direction%";
"routing_nodistance_straightahead" = "Straight ahead";
"routing_nodistance_nowbearX" = "now bear %direction%";
"routing_nodistance_changetoXahead" = "change to %direction%";
"routing_nodistance_nowchangetoXahead" = "now change to %direction%";
"routing_nodistance_destination" = "destination ahead";

"routing_minutes_short" = "min";
"routing_meters_short" = "m";
"routing_kilometers_short" = "km";
"routing_feet_short" = "ft";
"routing_miles_short" = "mi";

Sideview

If used with the FavendoMapEngine framework, the SDK also offers a sideview implementation, that shows the venue from the/a side and highlights the currently shown camera-position, among other things.

The class (FAV)SideviewScrollView contains everything that is needed, all gesture-handling is done internally without setup.

Here is the supported feature list:

  • zooming (can be disabled)

  • a tap anywhere on the sideview leads to the linked map-instance to scroll there

  • via long-press and drag, the map-instance can also be scrolled continously

  • two rectangle shows the current displayed camera-bounding-box:

    • one at the top of the sideview with fixed height

    • one that is placed at the position of the displayed level, also indicating the height of that level

  • markers can be added to any level, with custom images

  • additionally a position-marker can be displayed, with a custom image

  • display routes as paths

Sideview Example

Here is a quick setup-guide to get starting with using the sideview.

This does not work if Google Maps or any other than the internal Favendo map-engine is used!
class ViewController {
    var sideviewScrollView: SideviewScrollView!
    var mapView: FAVIndoorMapview!

    func setupSideview() {
        sideviewScrollView = SideviewScrollView()
        sideviewScrollView.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(sideviewScrollView)
        view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[sideviewScrollView]|", options: [], metrics: nil, views: ["sideviewScrollView": sideviewScrollView]))
        // shows the mapview on top and the sideview at the bottom
        view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[mapView][sideviewScrollView(==150)]|", options: [], metrics: nil, views: ["mapView": mapView, "sideviewScrollView": sideviewScrollView]))

        view.layoutIfNeeded()

        let filepath = Bundle.main.path(forResource: "sideview_info", ofType: "json")
        sideviewScrollView.setupSideview(with: UIImage(named: "sideview_image")!, fileUrlToJson: URL(fileURLWithPath: filepath!))
    }
}

In order to update the displayed camera-bounding box in the sideview, forward map-events like so:

/// from FAVIndoorMapview.mapViewEventDelegate
extension ViewController: FAVIndoorMapSDKEventDelegate {
    func mapViewIdle(atCamera camera: FAVIndoorMapSDKCameraWrapper) {
        // the sideview will retrieve the camera-bounds internally from its linked map-engine
        sideviewScrollView.mapViewIdle()
    }
}

extension ViewController: FAVIndoorMapviewDelegate {
    func detectedLevelChange(toNewLevel newLevel: Int) {
        levelChanged(to: newLevel)
    }

    func userDidSelectLevel(viaLevelSwitcher levelNumber: Int) {
        levelChanged(to: newLevel)
    }

    func levelChanged(to levelNumber: Int) {
        // retrieve and cast the map-engine from the MapSDKs wrapper
        // this will fail, if a different engine than FavendoMapEngine is used
        let mapEngine = mapView.mapViewWrapper.mapView as! FavendoMapEngine
        sideviewScrollView.setDisplayedLevel(levelNumber, mapEngine: mapEngine)
    }
}

The above will setup the sideview and places it underneath the mapview. There are two lines that are especially important:

  • sideviewScrollView.setupSideview
    which sets up the sideview, as nothing is displayed before that. In here, the image and a (local) URL to the sideview JSON need to be provided. the actual format of the JSON is not defined here, you should be provided with sufficient information by your Favendo representative.

  • sideviewScrollView.setDisplayedLevel(:mapEngine:)
    if the level is changed in the MapSDK, a new engine is created for technical reasons. So the map-engine needs to be set again. also, one of the rectangles shows the visible camera bounding-box at the displayed level. For that, it obviously needs to know which level is currently displayed.

Furthermore, the sideview offers functionality to display custom markers:

func updateUserPosition(to coordinate: CLLocationCoordinate2D, levelNumber: Int) {
    sideviewScrollView.userPositionIcon = UIImage(named: "compass")!
    sideviewScrollView.updateUserPosition(coordinate, levelNumber: levelNumber)
}

/// this will add, or update a marker with the given identifier
func addUserMarker(identifier: String, coordinate: CLLocationCoordinate, levelNumber: Int) {
    sideviewScrollView.updateSideviewMarker(withIdentifier: identifier, icon: UIImage(named: identifier)!)
    sideviewScrollView.updateSideviewMarker(withIdentifier: identifier, coordinate: CLLocationCoordinate2DMake(markerPositionLat, markerPositionLon), levelNumber: randomLevel())
}

Lastly, it is possible to display a navigation-route on the sideview:

func dislayNavigationRoute() {
    let navigationPathLocations: [CLLocation] = [
        CLLocation(coordinate: CLLocationCoordinate2DMake(20.15053703437028, -7.799980576087561), altitude: 5.0, horizontalAccuracy: 0.0, verticalAccuracy: 0.0, timestamp: Date()),
        CLLocation(coordinate: CLLocationCoordinate2DMake(20.15069328864032, -7.799773258127852), altitude: 5.0, horizontalAccuracy: 0.0, verticalAccuracy: 0.0, timestamp: Date()),
        CLLocation(coordinate: CLLocationCoordinate2DMake(20.15031778581602, -7.799631451500462), altitude: 5.0, horizontalAccuracy: 0.0, verticalAccuracy: 0.0, timestamp: Date()),
        CLLocation(coordinate: CLLocationCoordinate2DMake(20.15036342571697, -7.798982758756681), altitude: 5.0, horizontalAccuracy: 0.0, verticalAccuracy: 0.0, timestamp: Date()),
        CLLocation(coordinate: CLLocationCoordinate2DMake(20.15036342571697, -7.798982758756681), altitude: 6.0, horizontalAccuracy: 0.0, verticalAccuracy: 0.0, timestamp: Date()),
        CLLocation(coordinate: CLLocationCoordinate2DMake(20.1509009526318, -7.800022726061453), altitude: 6.0, horizontalAccuracy: 0.0, verticalAccuracy: 0.0, timestamp: Date())
    ]

    // will draw a purple route with width 5 screenpoints (regardless of zoom-state in sideview)
    sideviewScrollView.drawNavigationPath(navigationPathLocations, color: UIColor.purple, width: 5.0)
}

Troubleshooting / FAQ

.framework contains unsupported architectures [x86_64, i386] (iTunesConnect)

If an app with the MapSDK (or BackspinSDK) is submitted to Apple, then you need to make sure that there are no simulator-slices in any binaries left due to an App Store submission "bug". If this happens, then you probably do not use Cocoapods or at least one framework is embedded manually.

See the next section about removing the unused architecture-slices.

this script will also remove all slices from the BackspinSDK, if used.

Can the SDK size be reduced?

First, if you are not using GoogleMaps, you can use the version that is compiled without GoogleMaps support, which has a reduced framework-size.

By default, the framework contains slices for both the device (arm) and the simulator (x86_64/i386). Unfortunately, Xcode does not strip out unneeded slices from frameworks if the app is compiled/archived.

Luckily, the SDK contains a script which strips unneeded slices. Go to your target app in the project view and "Build Phases". At best at the very end but at least after the "Embed Frameworks" (or any CocoaPods copy frameworks) build phase, insert a new "Run Script Build Phase" with the following content:

${SRCROOT}/PATH_TO_SDK_EXTRACT_FOLDER_ZIP/strip_backspin_slices.sh
e.g. if used with Pods
${SRCROOT}/Pods/Favendo-iOS-Mapview/strip_backspin_slices.sh

Adapt the path to the shell-script and you are ready to go. You may look into the script for more information.

Alternatively if you are using Carthage you may use its script that removes unused slices: see 4. and after with carthage copy-frameworks

How to toggle the marker-detail-view

If a marker is selected, there will be a callback in the FAVIndoorMapviewDelegate to inform about that event. By default nothing happens.

So you need to do 2 things: set the marker-state as "selected", toggle the bottom-view, and return true so the map centers the just tapped marker:

func shouldScrollToMarkerThatHasBeenTapped(for markerWrapper: FAVIndoorMapsMarkerWrapper) -> Bool {
    if mapView.markerInfoBottomView.update?(withMarker: markerWrapper) ?? false {
        // will
        markerWrapper.markerState = .selected
        mapView.toggleMarkerInfoBottomView(true)
    }
    return true
}

How to tint selected markers?

See the prior section.

Can I access the GMSMapView?

In case Google Maps is used, the FAVIndoorMapview object has a property googleMapsMapView which returns the current GMSMapView instance.

Beware, however, that certain settings or configurations might interfere with the handling of the FAVIndoorMapview.

For each level-change the map-engine (i.e. GMSMapView in this case) is created as new instance, so all custom settings are lost. It is recommended to perform any customisations directly in the map-delegate detectedLevelChangeToNewLevel: as in the following example:

The following example accesses the GMSMapView after each level-change and sets a custom map-style.

let kGoogleMapsStyleJson = "[{" +
    "\"featureType\": \"poi.attraction\", \"stylers\": [{\"visibility\": \"off\"}]" +
    "}]"

func detectedLevelChange(toNewLevel newLevel: Int) {
    if FAVIndoorMapview.usedMapEngine() == .googleMaps {
        // setup map-style with hidden POIs of type "attraction"
        mapView.googleMapsMapView?.mapStyle
            = try! GMSMapStyle(jsonString: kGoogleMapsStyleJson)
    }
}
For custom Google Maps map-styles, there is also a designated convenience callback googleMapsMapStyle in the FAVIndoorMapviewDelegate which is called automatically for each new Google Maps instance. See also Google Maps iOS SDK: Quick Map Styling

Do I need to embed the framework?

Yes, since v1.0.9 of the MapSDK it is a dynamic framework, which needs to be embedded into the final app.

Can I support iOS 7?

Since the MapSDK is a dynamic framework, it requires iOS 8 as minimum deployment target.

My custom map markers / overlays are lost on level-changes

For memory management purposes, the GMSMapView instance is destroyed and newly initialized if a level-change is triggered. All items that are bound to a GMSMapView are therefore lost as well. If you keep a strong reference to any object, it is still there, but you might have to set the .map attribute again to the new Google Maps instance.

As far as map-markers are concerned, it is highly recommended to use the Favendo wrapper classes. If you desire any functionality that is not being offered, you are very welcome to propose respective extensions.

If you add marker-wrappers via addPermanentMarker: on the mapview, these markers are kept during level-changes and will re-appear on the set level again.

Changing the visbility of a marker on runtime does not work

Markers are only drawn or added to clustering if updateClusteringForMarkersForCurrentLevel is called on the mapview. If the marker is currently shown on the map and its visibility is changed to not visible, then the marker triggers a new clustering automatically. But this only works if the marker is really SHOWN, not just theoretically visible. Clustered markers do not have a reference to the map and therefore also cannot update the clustering.

It is a good rule of thumb to always call updateClusteringForMarkersForCurrentLevel if any map-object has changes (icon, ground-anchor, visibility, …​).

iTunesConnect: Non-public API usage

If the app is rejected due to "non-public API usage" in the FavendoIndoorMapview framework, try using the No-Gmaps Version of the Map-SDK. This means, you need to use the Favendo map-engine instead of GoogleMaps.

You may also try to object the rejection with the following explanation:

In the favendo MapSDK there are wrappers for our own (=favendo) Map-Engine was well as GoogleMaps. Due to GoogleMaps being a static library, there is a workaround necessary to keep it "weak linked" (i.e. link the static library only with the final app-target and not our SDK already), all GoogleMaps classes are referenced via the ObjC runtime as "NSClassFromString" - to compile everything, the class-symbols are defined as "friend"-classes derived from NSObject

Excerpt from iTunesConnect issue mail:

"Non-public API usage:

The app references non-public selectors in Frameworks/FavendoIndoorMapview.framework/FavendoIndoorMapview: coordinateForPoint:, isTappable, setSpans:, setUserAgent:"

The following code-snippet shows all these usages from GoogleMaps:

HEADER DEFINITIONS

// do not import GoogleMaps, instead define friend-classes
//#import <GoogleMaps/GoogleMaps.h>

@interface GMSOverlay: NSObject
@end

@interface GMSURLTileLayer: GMSOverlay
@property NSString *userAgent; //<<<<<<<<<<<<<<<<<<<<
@end

@interface GMSMapView: UIView
@end

@interface UIView (FriendClass)
- (CGPoint) pointForCoordinate:(CLLocationCoordinate2D)coordinate;
- (CLLocationCoordinate2D) coordinateForPoint:(CGPoint)point; //<<<<<<<<<<<<<<<<<<<<
@end

@interface GMSCircle: GMSOverlay
@property BOOL isTappable; //<<<<<<<<<<<<<<<<<<<<
@end

@interface GMSPolyline: GMSOverlay
@property NSArray *spans; //<<<<<<<<<<<<<<<<<<<<
@end

IMPLEMENTATIONS

self.tileUrlLayer = [NSClassFromString(@"GMSURLTileLayer") tileLayerWithURLConstructor:tilesUrlConstructor];
self.tileUrlLayer.userAgent = @"SDK user agent";


- (CGPoint) projectionPointForCoordinate:(CLLocationCoordinate2D)coordinate {
    return [self.googleMapsMapView.projection pointForCoordinate:coordinate];
}
- (CLLocationCoordinate2D) projectionCoordinateForPoint:(CGPoint)point {
    return [self.googleMapsMapView.projection coordinateForPoint:point];
}

self.circle.isTappable = false;
self.polyline.spans = @[[NSClassFromString(@"GMSStyleSpan") spanWithStyle:strokeStyle]];

The wrong polygon/marker is tapped (Favendo Map-Engine)

This is only relevant for the Favendo Map-Engine.

There might be the case that the wrong map-overlay is selected if they have overlapping bounding-boxes. This is especially relevant for large polygons. The reason is that the Favendo map-engine uses a performance-optimized hit-test that will only consider the bounding-box of elements and returns at the first positive match.

Usually, especially if marker-clustering is enabled, this is sufficient. But in some cases the results are undesired. See also SCNHitTestOption.boundingBoxOnly

The search-behaviour can be changed to use a less performant test, that is more accurate:

if let favendoMapEngine = indoorMapview.mapViewWrapper?.mapView as? FavendoMapEngine {
    favendoMapEngine.hitTestBoundingBoxOnly = false
}

You need to set this setting after each level-change, so at best register to the level-change callback of the FAVIndoorMapview:

func detectedLevelChange(toNewLevel newLevel: Int) {
    if let favendoMapEngine = indoorMapview.mapViewWrapper?.mapView as? FavendoMapEngine {
        favendoMapEngine.hitTestBoundingBoxOnly = false
    }
}