List of Functions

Provided by the iOS SDK

Positioning

The SDK offers beacon based indoor-positioning including bluloc-exclusive crypto-beacon (rolling-id mode) support. It includes beacon-region monitoring to automatically start positioning and beacon-proximity ranging (even in the background with some restrictions) once the user enters a beacon-region.

The positioning can be configured with a number of "accuracy"-settings for their respective use cases (e.g. a power-saving-mode if positioning is in background for location-based-notifications only, and a "best accuracy" preset if the position is visible for the app-user)

Since the monitoring is done by iOS, it is impossible to precisely predict when exactly the app/SDK will be woken up and receive a region entered event. A number of factors affect the delay between the enter event and its notification: Battery state (or the in iOS 9 introduced Low Power Mode), the number of apps on the device monitoring beacon-regions, the overall memory-consumption, and how recently the device booted. Technically, the delay can take up to 15 minutes. However, empirically the delay usually takes around 10-30 seconds.

Some iOS restrictions apply:

  • No more than 20 regions PER APP are allowed, including custom created geo-regions, e.g. CLCircularRegions . Each unique beacon-UUID leads to 1 region being monitored.

  • See Apple Documentation for Region Monitoring: "Be judicious when specifying the set of regions to monitor. Regions are a shared system resource, and the total number of regions available system wide is limited. For this reason, Core Location limits the number of regions that may be monitored simultaneously by a single app. To work around this limit, consider registering only those regions in the user’s immediate vicinity."

  • As mentioned in the section Custom Region Monitoring, you need to add all custom UUIDs that are monitored with an own CLLocationManager to the whitelist of BackspinSDKController: whiteListBeaconUuidsForBeaconMonitoring.

  • Unless the Background-Mode for Locations is enabled in the app/Xcode, the app will resign once it is not in the foreground any more. You will get around 10s of activiy if a beacon-region is entered (or exited), but for anything else, the background-mode is required. Since BackspinSDK v3.1.0, there is an extended background-mode using GPS, see also Can I receive SDK notifications and position updates in the background? and Background Beacon Ranging

Navigation

Turn By Turn in Whitelabel

The SDK provides a navigation-path between two given coordinates. Initially, both coordinates are mapped onto the navigation path, the actual route is subsequently calculated between these two intersection points. A set of target coordinates can also be defined, from which the nearest one will be targeted - e.g. to navigate to a shop that has multiple entrances. In case a suitable KML is provided (see Backspin web-frontend documentation, the route can be requested with additional qualifications such as barrier-freedom.

Likewise, additional turn-by-turn-directions to aid the visual output can be requested for any given route, as in the example screenshot of the Favendo MapviewSDK in the Whitelabel App.

Image Downloader & Level/Floor-Plans

The SDK provides methods to download and cache images with custom resolutions as well as level-plans with their respective images.

If a language is specified via the currentLanguageDesignator delegate method, only images with the corresponding language identifier will be downloaded provided that they have been previously uploaded through the Backspin Web-Frontend. Images for each language and resolution are cached separately via NSURLCache.

Location Based Notifications

There are two different types of notifications supported by the SDK:

Beacon Proximity

  • In this case, beacons trigger notifications if they are approached. The required proximity can be qualified as IMMEDIATE, NEAR, or FAR. These values are approximations for distances around 0.5 meter, 3-5 meters, and 5+ meters. The exact distances at which notifications are fired might vary with each setup and beacon.

An enter event for a notification defined as FAR is also triggered if one is in the NEAR or IMMEDIATE proximity range.

Zone/Location-Based

  • A location based notification can be configured in the web-frontend as a venue or offer based notification.

  • Zone and location-based trigger types come in three flavors: Enter, Exit, and Dwelltime. The latter will trigger if the user has spent a previously specified amount of minutes (max. 120) within the given zone or at the given location.

  • While offer- and venue-triggered notifications are separated in the web-frontend, they are internally treated identically, since all notifications refer to their respective venue location. Therefore, if an offer does not have any linked venue, it cannot be processed.

if you require the SDK to fire notifications in the background, make sure to enable a background-mode as described in Background Beacon Ranging

Apple Push Notifications / App Account

Backspin can send push notifications via the Apple Push Notification Services (APNS). As explained in the Apple Documentation (or e.g. at Ray Wenderlich), you need to enable the service in the Apple Member Center, upload the certificate to the Backspin server (to do this, please contact your favendo project manager. S/He is gonna do that for you) and implement the UIAppDelegate callback:

- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;

The deviceToken can be used to call the SDK setup method to link it with its implicitly created user-account. Alternatively, it can be manually linked with registerDeviceWithApnsTokenAtServer:

There is a distinction between user-account and app-account. The app-account is set up on startup and linked with the APNS-token to send pushes and save the current language. The user-account can be used to handle user-specific data like the Backspin user profile, likes, or leaflet. As of now these two accounts have the same ID.

User Account

Not to be mixed up with the app-account described in Apple Push Notifications / App Account.

For now, the user-account is identical to the app-account. It is a placeholder for an upcoming feature which will allow sharing of an account across multiple devices.

It is used to hold user-specific data like a likes-list or account-profile. See more at User Account.

Analytics

For several events in the SDK, analytics can be generated and transmitted to the server. Analytics can be configured for each event type independently. The following types are supported out-of-the-box:

  • Indoor Location Updates
    is created automatically
    internal key location_updated

  • Notification created
    is created automatically, right before the notification-object is handed out to the SDK-user
    internal key notification_created

By implementing the BSSDKAnalyticsTypeHandler protocol, custom-defined analytics can be transmitted to the server through SDK by calling queueAnalyticsEventWithType:andAnalyticsData: The events will be queued and persisted until they are successfully uploaded. See section Analytics in the web-frontend manual on how to see these events in the Backspin web-frontend.

Note that analytics are disabled by default, it needs to be enabled via BackspinSDKController.sharedInstance().analyticsEnabled. If enabled, all types as listed above are enabled, but can be disabled separately.

Model-Store & Server-API

The Backspin iOS SDK contains API to the model store to access all data that is downloaded from the SDK (venues, venue-categories, offers, …​). There is API to configure the requested data-set.

Asset Tracking

The SDK enables the usage of "asset tracking" (e.g. tracking persons with bluetooth bracelets, or anything that has a beacon attached to it). The API provides access to get the asset-positions, as well as names of the zones an asset is currently in. Furthermore one can register callbacks to be continously informed about updates, alerts (enter/exit events for defined zones). See Asset Tracking.

Prepare Your Project

Since the SDK uses Swift, you need to enable the “Always Embed Swift Standard Libraries” setting for your app. For more detail, see the Manual Installation section below.

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

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.

Since the SDK contains Swift code, you need to configure CocoaPods to use frameworks and work with iOS 8.0 upward. Insert the following at the top of your podfile:

platform :ios, "8.0"
use_frameworks!
This setting might affect other pods.

Manual Installation

The frameworks can also be installed manually.

Download the zipped framework from the link generated at the Favendo iOS SDK Download page (Backspin v3 with 3.12.4) that matches your Xcode version.

Extract the ZIP and add all frameworks from the /Framework folder to your framework as embedded: Select your target(s), go to “General” and drag the following frameworks from the folder above into the “Embedded Binaries” section:

  • BackspinSDK.framework

  • FavendoCommon.framework

  • FavendoBackspinCore.framework

  • FavendoPositioning.framework

  • FavendoNavigation.framework

External Dependencies

The Backspin iOS SDK has one external framework dependency that needs to be added to your project manually:

The SDK is linked with the following version of this framework:

  • AFNetworking: 3.2.0

The SDK has been tested to work with AFNetworking 2.4+, in particular 2.4.1, 2.5.4, and 2.6.3, though there is no guarantee that every feature works as reliable as with 3.1 or 3.2.

Adjust Project Settings

  1. In Xcode, select your project and target.

  2. Select the Build Settings tab.

  3. Search for “Other Linker Flags” in the provided search bar.

    • Add -ObjC to the “Other Linker Flags” setting.

    • Add -l “sqlite3” to the “Other Linker Flags” setting

  4. Search for “Build Options”

  5. Set “Always Embed Swift Standard Libraries” to “Yes

  6. Set “Enable Modules (C and Objective-C)” to “Yes

  7. Set “Link frameworks automatically” to “Yes” or follow the next section

Further Requirements

If “Link frameworks automatically” is set to “No”, these Apple frameworks need to be added to the project manually:

  • Accelerate.framework

  • AudioToolbox.framework

  • CoreLocation.framework

  • CoreMotion.framework

  • Foundation.framework

  • ImageIO.framework

  • MapKit.framework

  • MobileCoreServices.framework

  • Security.framework

  • SystemConfiguration.framework

  • UIKit.framework

Enable Location Services

To be able to access the current location of the user, you have to ask about permission to do so.

See a detailled explanation in Location Services Authorization.

Custom Region Monitoring

If you do custom Beacon Region Monitoring with the CLLocationManager, you should notify the BackspinSDK about the regions that are monitored. When starting monitoring/positioning, all current regions are removed from monitoring and the current positioning-regions are added again. This is done to ensure there are no dangling/unused regions if the positioning-UUID is changed.

Obviously this leads to the removal of any custom created beacon-regions that are monitored somewhere else in the app. You need to whitelist these regions for the SDK to keep them. Before calling setupBackspinWithApiKey, you need to add your UUIDs to this set:

// add this UUID to the whitelist, so it will be kept during monitoring-region-cleanup
BackspinSDKController.sharedInstance()
        .whiteListBeaconUuidsForBeaconMonitoring
        .add("CUSTOM_UUID_THAT_IS_MONITORED")

Geo-Regions (CLCircularRegion) are NOT affected by the cleanup.

Start Using the Backspin iOS SDK

Import the SDK Header

In order to use any functionality of the Backspin iOS SDK, make sure you import the module

import BackspinSDK

or in Objective C the public framework header

#import <BackspinSDK/BackspinSDK.h>

Setup the Controller

The BackspinSDKController is the main interface for communicating with Backspin.

  • setup the environment

  • download Backspin models

  • download level-plans and images from Backspin

  • start indoor location updates

  • calculating navigation paths

  • handle notifications

  • etc.

For an extensive list of functions, see the sections List of Functions

In the following section you’ll see how to set up the controller and implement a delegate method to receive location updates.

Remember to call setupBackspin(withBasicAuth:) before any other call, the SDK will throw exceptions if unauthenticated.

Two properties should be set before calling setupBackspin(withBasicAuth:)

1.) The root-scope-id, otherwise a random scope-id will be used for the initial model-download (this is relevant only if you have more than one root-scope-id, e.g. multiple root-venues).

2.) As mentioned in Custom Region Monitoring, add custom monitored beacon UUIDs to whiteListBeaconUuidsForBeaconMonitoring.

The SDK will not download data automatically! While the setup method triggers the download of root-venues and creation of an app-account, it is required to call triggerContentUpdateFromServer: or
requestModelDownloadOrUpdateForRestEndpoint: before using positioning, navigation, or notifications. See also Model-Store & Server API for the API to request models from the server.

In the code-snippet it is implemented directly in the app-delegate for simplicity, but you might want to have an own class like BackspinHandler which handles/wraps everything.

In the following example, an APNS token is requested from Apple as well, you might not need that if you do not use APNS pushes from Backspin.

class MyAppDelegate: UIApplicationDelegate {
    /// optional, only if APNS pushes are used via Backspin
	var apnsData: Data?

    /// optional, only if APNS pushes are used via Backspin
    func application(_ application: UIApplication,
    	didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        self.apnsData = deviceToken
    }

	func setupBackspin() {
		// see the following snippets
		BackspinSDKController.sharedInstance().delegate = self
		BackspinSDKController.sharedInstance().datasource = self

   		// set the root-scope-id, otherwise a random one will be selected
    	// only relevant if there are multiple root-scopes
    	BackspinSDKController.sharedInstance().setRootScopeId(23)

    	// add this UUID to the whitelist, so it will be kept during monitoring-region-cleanup
    	// only relevant if you are using a beacon-UUID that is different from any UUID managed by Backspin
    	BackspinSDKController.sharedInstance()
    		.whiteListBeaconUuidsForBeaconMonitoring
    		.add("CUSTOM_BEACON_REGION")


    	// authenticate the SDK with the given basic auth key against the PROD/LIVE server
    	// use either BSSDKServerInstanceUAT or BSSDKServerInstancePROD,
    	// depending where your account is created
    	BackspinSDKController.sharedInstance().setupBackspin(
    		withBasicAuth: "YOUR_BASIC_AUTH_KEY",
    		forServer: .PROD,
    		andApnsTokenData: self.apnsData, // can be left nil if no APNS pushes are used via Backspin
    		success: {

        	// download data now (required for positioning to start!)
        	BackspinSDKController.sharedInstance().requestRootModelTypesDownloadOrUpdate(
            	[
            	// additionally, download venues and venue-categories
                BSGenericModelSQLite.modelTypeVenue(),
                BSGenericModelSQLite.modelTypeVenueCategory()
            	], language: "de", // in German
            	completionBlock: nil)

        	// enable the adaptive positioning preset
      		BackspinSDKController.sharedInstance()
      			.enable(BSSDKPositioningAccuracySetting.adaptive)

			// now start region monitoring
			// the beacon-ranging (and indoor-positioning) will start, if a beacon is
			// monitored with a UUID as configured in the Backspin web-frontend
			BackspinSDKController.sharedInstance()
				.startMonitoringBackspinPositioningBeacons()

			// alternatively, beacon-ranging can also be started immediately
            //BackspinSDKController.sharedInstance()
            //	.startIndoorPositioningWithBackspinPositioningBeacons()

        }) { (error: Error?) in
        	// Handle error
            if let error = error {
                print("There was a backspin error? \(error)")
            }
        }
}
If you are not using PROD or UAT, you may also use a custom-server-url. For that, you have to import the module for "extended servers":
  • access in Swift via import BackspinSDK.ExtendedServers

  • access in ObjC via #import <BackspinSDK/BackspinSDKController+ExtendedServers.h>

import BackspinSDK.ExtendedServers

BackspinSDKController.sharedInstance().setupBackspin(
    withBasicAuth: "YOUR_BASIC_AUTH_KEY",
    forServerUrl: "https://custom-client.commander.favendo.com:443/backspin-backend/",
    success: {
        // logic in here
    })

React to Events

extension MyAppDelegate: BackspinSDKControllerDelegate {
	func calculatedNewIndoorLocation(_ indoorLocation: CLLocation) {
		// assume a mapview is currently visible
		// adapt the positioning preset corresponding to the level-number

		if indoorLocation.altitude ==
			MYMAPVIEWCONTROLLER.displayedLevelNumber {
			// use adaptive positioning if the user-position is on the current displayed level
			BackspinSDKController.sharedInstance()
					.enable(BSSDKPositioningAccuracySetting.adaptive)
		}
		else {
			// otherwise use the power-saving preset for "low-accuracy" updates
				BackspinSDKController.sharedInstance()
					.enable(BSSDKPositioningAccuracySetting.powerSaving)
		}
	}

    func handleAppNotification(_ appNotificationModel: BSAppNotificationModel) {
        if #available(iOS 10.0, *) {
        	// present an iOS 10 notification. Omitted for simplicity
	    	// see introduction https://www.appcoda.com/ios10-user-notifications-guide/
        }
        else {
    		// legacy iOS 9 way of presenting notifications
        	let localNotification = UILocalNotification()
        	localNotification.alertTitle = appNotificationModel.notificationTitle
        	localNotification.alertBody = appNotificationModel.notificationBody

        	UIApplication.shared.scheduleLocalNotification(localNotification)
        }
    }
}

Data Source

extension MyAppDelegate: BackspinSDKControllerDatasource {
    func currentLanguageDesignator() -> String {
    	// if possible, the models should be downloaded in Amurican English
        return "en"
    }

    /// to reduce traffic, you can put the level-model inside the shipped app-bundle directly
    func levelPlanImage(forLevel levelModel: BSSDKLevelPlanModel) -> UIImage? {
    	return UIImage(named: "levelplan_with_levelnumber_\(levelModel.levelNumber)")
    }
}

For further implementation details look at the public header files, or look at the legen …​ (wait for it)

Demo App

…​ dary sample app! Ask your favendo contact person for credentials for the demo app repository. You will find a sample implementation app including mapview and asset-tracking. Via the mapview you will see (almost) the full range of features supported by the BackspinSDK as well as the favendo MapSDK.

Positioning

General

The following methods are required even if your App is confined to beacon-proximity-notifications (without indoor-positioning). In the end all methods will trigger/handle beacon-ranging.

If no positioning-beacons are available in the backend but notifications are linked to proximity beacons, monitoring will be started automatically.

Beacon monitoring does require enabling Enable Location Services and setting the AlwaysUsage key in the Info.plist! WhenInUse will require the SDK user to trigger beacon ranging manually via the startIndoorPositioningWithBackspinPositioningBeacons method.

Location Services Authorization

To be able to access the current location of the user, you have to ask about permission to do so. For that, a custom description needs to be added to the Info.plist that explains why your app is using location-services. It is commended to add either the first or all of the following three keys, which’s value should be a description why the app needs to access the current location. A sample description would be "Your location is needed to provide information about stores and special offers around you."

  • NSLocationWhenInUseUsageDescription
    this key is necessary to allow the app to access GPS locations and perform beacon-ranging while it is in use. Usually this means, while the app is in foreground.

  • NSLocationAlwaysUsageDescription
    if you want to use iBeacon Monitoring in addition to beacon-ranging, you need to have the Always authorization. You can add the same description as for the WhenInUse key.

  • NSLocationAlwaysAndWhenInUseUsageDescription
    this is a new key for iOS 11 support, which is the equivalent of the Always key. In iOS 11 the user can now choose themselves between WhenInUse and Always, so this is a combined key.

For beacon-region-monitoring the NSLocationAlwaysUsageDescription (and NSLocationAlwaysAndWhenInUseUsageDescription) key needs to be added. For background updates, you should also add Continued use of GPS running in the background can dramatically decrease battery life to the App-Store description. While you can translate it to each supported language, it is highly recommended to keep the original in English as well for the review process.

In order to work with indoor-positioning/location-based-content/beacon-proximity in the background, you need to enable the “Location Updates” Background-Mode for iOS 9 and later. If you do not use any of these features in background mode, you should restrain from setting this flag. Otherwise it is likely for Apple to question its use during the review process. See more detail in the Troubleshooting / FAQ section and the official documentation Apple Documentation for CLLocationManager.

Background Modes

iOS 11 Handling

iOS 11 Popup

In iOS 11 a new "privacy concept" was introduced for app-users. Before, starting from iOS 8, the developer could decide if the app needs to have Always or WhenInUse authorization and the user had to accept that decision - either allow an app the developer’s desired authorization-level, or have no location-services for an app at all.

However, with iOS 11 the user can select between Always and WhenInUse, regardless of what the app/SDK developer requested. Even "worse": The way iOS presents authorization-popups, it is more or less very suggestive to select WhenInUse.

Why is this "bad" you ask? Not at all, if you do not use iBeacon Monitoring. If you only use positioning while you are in a mapview and stop beacon-ranging if you leave it, for example, you only need the WhenInUse authorization anyway, also because the app is probably always open while the beacon-ranging is performed.
However, if you want to use the monitoring-capabilities, have the beacon-ranging start and stop automatically and even have the app woken up when it is in the background or not even active, then you need the Always authorization. Even while the app is active and in foreground, monitoring will not work without the Always authorization.

First the good news: If the app only has WhenInUse authorization and you use the BackspinSDK monitoring API (e.g. startMonitoringBackspinPositioningBeacons or startMonitoringPositioningBeaconRegionWithUUID:), then the SDK will automatically start beacon-ranging, even if no Always authorization has been granted and monitoring cannot be started. You might want to reduce the beacon-ranging to when the user actually is in the beacon-region.

For a better user-experience Apple proposes a two phase approach of requesting authorization. First, apps should request WhenInUse authorization to make the user familiar with its features and the app’s value. So everything in the app should/must work with WhenInUse only, while the app is in usage - the BackspinSDK supports this by offering ranging-only without monitoring.

The app developer can then present the user with details why Always authorization is beneficial for the app (e.g. a page in an "App Walkthrough" during intial setup, or via popup) - the app/SDK can then request Always authorization which leads to another iOS system popup.

If the two-phase approach is ignored and the app requests Always directly from the beginning, the popup above will be shown once and the app is stuck with whatever option the user chooses, since most people won’t go to the settings and change the app-specific location-services settings afterwards.

The two-phase approach is downward compatible to iOS 8, so it can be used globally regardless of the iOS version. This approach is supported by the BackspinSDK via the authorizationStatusToRequest flag, which should first be set to .whenInUse and afterwards to .always:

// setup the BackspinSDK
BackspinSDKController.sharedInstance().setupBackspin(
    withBasicAuth: "YOUR_BASIC_AUTH_KEY",
    forServer: .PROD,
    andApnsTokenData: nil,
    success: {

        // ...

        // set this before any positioning/monitoring call is made!
        // at this point, the iOS Location Services Authorization popup will be presented for "WhenInUse"
        BackspinSDKController.sharedInstance().authorizationStatusToRequest = .whenInUse


        // now present a UI to offer the user an explanation why "Always" authorization is beneficial.


        // make a second request to get "upgraded" to "Always" authorization
        BackspinSDKController.sharedInstance().authorizationStatusToRequest = .always
})

iOS 11 Request

Rather than an alert-view it is recommended to present a view, possibly as blur-view overlay. After the user confirms, you can request the Always authorization.

To increase battery-life, it is recommended to use iBeacon monitoring, so the app knows when to start and stop scanning for beacons.
For that the app needs your allowance to "access your location" always.
The app won’t access your GPS location or scan for beacons if you are outside of the beacon-zone (Shopping-Center, Airport, Trainstation or whatever this app was designed for). The app will be woken by iOS if you enter the zone, so all the location-based services are available automatically.

iBeacon Monitoring

Since iOS 7.1, beacon region monitoring is reliable and responsive enough to be preferable to manually starting positioning ranging.

If the positioning is only used to display the position on the map and will be stopped if the map is invisible, monitoring might not be needed. In scenarios that require beacon responsiveness in the background, e.g. for proximity-notifications (see Location Based Notifications) which fire in the background, monitoring makes more sense.

Monitoring in iOS means, the app can be woken up by iOS and to start ranging/positioning and also deliver Location Based Notifications in the background. The app is active for up to 10 seconds while in background. After that time, the App will be shut down again, unless background modes for location are enabled. Keep in mind the considerations mentioned at Background Beacon Ranging and Notes about iOS Background Modes.

Two different methods for beacon-ranging/positioning can be used:

// monitoring, trigger ranging-start once the user enters the region
// requires the "AlwaysUsage" permission to work, even in foreground
// read out the UUIDs and crypto-config from the beacons in the local database once content has been loaded from Backspin
BackspinSDKController.sharedInstance()
                    .startMonitoringBackspinPositioningBeacons()

// start positioning/ranging NOW, regardless if beacons are around or not
// only requires the "WhenInUse" permission
// read out the UUIDs from the beacons in the local database once content has been loaded from Backspin
BackspinSDKController.sharedInstance()
                    .startIndoorPositioningWithBackspinPositioningBeacons()

There are three different methods to stop beacon-ranging / monitoring:

// this will stop monitoring of all regions
// if currently inside an active beacon-region and ranging is going on, this will be stopped immediately
BackspinSDKController.sharedInstance()
                    .stopMonitoringOfBackspinPositioningBeacons()

// if stopBeaconMonitoring is set to true, this is essentially a combination of the two methods above.
// Will stop beacon-ranging immediately and also remove the beacon-region from being monitored.
BackspinSDKController.sharedInstance()
                    .stopIndoorPositioning(
                        ofBackspinPositioningBeaconsAndStopMonitoring: true)

Accuracy Settings

There are settings presets which combine a number position-configurations to optimise the SDK for certain scenarios.

  • No Preset
    This is the default "preset", all positioning-configuration settings are set to their default values. Their appropriateness depends on the use case. If the user-position is not simply used for location-based-content notifications but directly displayed on a map, the default settings might not be ideal. While the API allows to override this preset, it is not recommended. (It can be thought of as an internal settings preset.)

  • Power Saving mode
    This preset is recommended if position accuracy is not of primary concern. It disables various filters to save computing power. A possible use case is location-based-content notifications that fire in the background. The effect of less accurate position updates on the outcome is negligible here, making it advisable to save battery life. Similarly, if the map displays another level than the current position, accuracy only to the extend of the current level/region is required. The settings in this preset are exhaustive, there is no need to change them manually.

  • Best Accuracy
    Ideal if the app-user is directly looking at her position on a map. While the default values represent a general "best practice", their ideality depend on the beacon-installation at sight. Trying out a variety of settings is recommended. You can also use setPositioningConfiguration:
    forSetting:forAccuracySetting:BSSDKPositioningAccuracySettingBestAccuracy to change presets for this setting.

  • Adaptive
    This preset is the same as Best Accuracy, but with the setting AdaptivePositioning enabled. See underneath for more explanation about adaptive positioning. Especially if the user is expected to change between a "walking" and "standing" state, this is the recommended setting for best stability.

Even if confined to a specific installation, there is not necessarily a perfect settings preset, as some areas/levels might work best with different settings. In a large space for instance, Navigation Path Snapping can lead to less accurate results than in an area with narrow paths. (An additional factor is the provided KML.) Your favourite favendo contact person will be happy to assist you with choosing a good setting preset for your location.

While the use of these presets are not required, they are recommended in terms of user experience.

// inside mapview (controller)
func calculatedNewIndoorLocation(_ indoorLocation: CLLocation) {
    if indoorLocation.altitude ==
        self.displayedLevelNumber {
        // use adaptive positioning if the user-position is on the current displayed level
        BackspinSDKController.sharedInstance()
                .enable(BSSDKPositioningAccuracySetting.adaptive)
    }
    else {
        // otherwise use the power-saving preset for "low-accuracy" updates
            BackspinSDKController.sharedInstance()
                .enable(BSSDKPositioningAccuracySetting.powerSaving)
    }
}

Positioning Configurations

These settings are explained in the BSSDKPositioningConfiguration enum which can be found in the header-file BackspinSDKControllerPositioningConfiguration.h. The default setting for each configuration per preset is listed there.

Each of these settings can be set once the success block of the setup method has been called. At that point, the accuracy-presets can be activated as well. The following list is roughly sorted by "effectiveness", the configurations at the top have more immediate impact to the position-result.

  • Use Best Number Of Beacons
    Only use the strongest X beacons for positioning. It is not recommended to have a value greater than 5.

  • Update Publish Interval
    The interval in which position updates are published. Since BackspinSDK v3.3.1 the position is still calculated every second, but might not be published that often. This results in a more smooth and stable experience for the user.

  • Rssi Smoothing
    Performs a statistical filter to smooth RSSIs based on the recent history for each beacon. Outliers and sudden fluctuations in signal strength usually do not influence accuracy significantly, but lead to delays and computational overhead. Advisable if very best accuracy is required. The history-size can also be set to create a smoother result, but this can also increase the delay of updates.

  • Sensible Rssi-Factor
    Filter out weak beacons relative to the strongest beacon. The factor defines how much weaker the RSSI of beacons can be before they are filtered out. E.g. if the strongest beacon has an RSSI of -65 dB and the factor is 1.2, then the weakest beacon that is allowed would have an RSSI of -78 dB.

  • Sensible Distance-Filter Speed
    To avoid positions jumps, it is possible to limit the distance a position can move between two consecutive location-updates. If a position moves further than X meters per second, the remainder will be cut off. Example: If this setting is set to 2.0 meters/second and UpdatePublishInterval to 2 seconds, a newly calculated position that is originally 10 meters away will cause this filter to cap the approximation to 4 meters towards this new position, not 10. Especially if set too low, this limits possible walking speed significantly and can hence lead to lagging. To minimize lagging, an internal check dynamically increases the speed setting if lags are detected. A fast average walking speed is 1.7m/s, so the default preset of 2.5m/s should fit most needs.

  • Use Beacons From All Levels
    The user’s position is calculated in the 2D space and gets enriched by a calculated level-number. Therefore, it might make sense to use beacons from all levels for the position-calculation. Otherwise the beacons are filtered and only the beacons are used from the level that was determined first.
    In general the strongest beacon(s) give(s) a good indication of the actual position, regardless of its level-number. So especially during level-changes this allows for a smoother transition. Most of the time there is no difference, but there are cases when it might be desired not to use beacons from all levels. At open areas where beacons from multiple floors can be ranged, this might lead to unwanted side-effects, e.g. the position will be displayed "in the air", hovering above a lower level.

  • Evolving Weighted-Average Location
    To provide more stability, this calculates another weighted average from the last X calculated positions. See next option. Can cause lags if the user is moving too fast.

  • Evolving Weighted-Average Location History-Size
    Define the number of location-updates from the history that should be considered for weighted averaging. Each second a new location is calculated (if the updateFrequency is HIGH).

  • Adaptive Positioning
    As an advanced measure for more stable positions, adaptive positioning can be enabled, which is a newer alternative to the MotionDetector setting. A walking-detector is used to determine if the user is standing or walking. If the user is standing for extended periods (some seconds), several other settings are raised to increase stableness. In a sense, adaptive positioning is a "combined" setting for several others that change during runtime.

    These other settings include:

    • .activateMotionDetectionFilter

    • .activateRssiSmoothing

    • .rssiSmoothingHistorySize

    • .activateEvolvingWeightedAverageLocation

    • .evolvingWeightedAverageLocationHistorySize

    • .smartLocationUpdateRadius

    • .activateBarometer

  • Rssi-Positioning-Threshold
    Filter out weakest beacons determined by this threshold before UseBestNumberOfBeacons is considered. Beacons that are too weak cannot improve accuracy.

  • Sane Beacon Distance Filter
    As an addition to the Sensible Rssi-Factor this filter processes the ranged beacons to prevent the usage of beacons that are significantly farther away than the others. Specifically, away from the "weighted" centroid. Can be used if it is expected or observed that far-away beacons are received strongly enough to pull the position away (e.g. on a large open space without beacons where beacons are received from the other side). Specify a distance after which beacons are to be filtered out. A recommended value is 10-15 meters, if problems are observed. By default this setting is disabled.

  • Smart Location-Update-Radius
    For a more stable positioning experience, this filter suppresses the publishing of position-updates that are still in the radius of the last position. If this filter is set to 3 meters, no position will be published that is less than 3 meters away. Note that if the SensibleDistanceFilter is less than SmartLocationUpdateRadius then at maximum every 2 seconds a new position will be published.

  • Navigation Path-Snapping
    Snaps the indoor-location to the nearest navigation-path to make the position marker more stable. This feature has been shown to be generally helpful. In certain unfavorable conditions however, for instance if the real position is right between two paths, the calculated position can jump back and forth between the two snapped positions. Since they are artificially snapped, the distance of those jumps can be larger than without this features. Also, keep in mind that path snapping only works if a KML (see dashboard documentation) has been provided. Enabling this configuration without a KML will render effectless.

  • Navigation Path-Snapping Distance
    Sets the threshold for path snapping. If the nearest navigation-path is beyond it, the path-snapping is ignored for this position update. Has been shown to be useful for large spaces with few paths which are fairly separated.

  • Motion-Detection Filter
    To make the user position more stable while at rest, the motion-detector can be activated. Prior to BackspinSDK version 3.3.1, this setting would completely suppress position updates, since v3.3.1 a mode is activated which results in a very strong smoothing while the user is detected not to be walking. The position will therefore be very stable and not jump suddenly. However, the position can still change over time, though very slowly. Note that level-changes might be detected with a delay that is higher than usual, e.g. while standing on an escalator without moving.
    This setting will change some other settings if the user is detected to be not walking:

    • .activateRssiSmoothing

    • .rssiSmoothingHistorySize

    • .activateEvolvingWeightedAverageLocation

    • .evolvingWeightedAverageLocationHistorySize

    • .activateBarometer

  • Motion-Detection Distance-Filter
    Prior to BackspinSDK version 3.3.1, the motion-detector would completely suppress position-updates, this distance-filter could ensure, however, that small updates are still published - to prevent that the position would freeze at a "wrong" coordinate. With the new strong smoothing, this problem is circumvented, so this feature should not be necessary. It can still be set to avoid big jumps while standing. See also SensibleDistanceFilterSpeed - if this value is set to a value greater than 0, it is used instead of SensibleDistanceFilterSpeed while the user is detected to be standing.

  • Optimised Circular-Lateration
    Calculates an estimated position based on distances to the nearest UseBestNumberOfBeacons beacons. While the default setting simply determines a weighted average (or a slightly moving "centroid"), this configuration will set up an equation system that is approximated based on the beacon-positions and their estimated distances. However, if beacons are far apart and the received RSSI values are weak, this setting can lead to worse results. A recommended beacon-density for this setting would be at least one beacon in a 3-4m radius.

  • Barometer
    Use the barometer (only available in iPhone 6 (Plus) and newer) to verify if detected level-changes were correct. If a level-change is detected via beacon-signals, it is checked if in the last 20 seconds at least BarometerDefaultLevelHeight meters were covered vertically. This will use the level-numbers from Backspin, while one level has the height of BarometerDefaultLevelHeight. The filter is most useful in large open spaces with unobstructed line-by-sight to beacons of other levels. If two artificial levels are added to Backspin with the same real "altitude", this can lead to delayed level-change-detections. If you experience problems with level-changes, disable this setting entirely.

  • Barometer Default Level-Height
    The default level-height that is used for the barometer check.

  • Beacon Tx-Power Value
    The TX Power value defines how strong a beacon is transmitting based on a reference measurement at 1 meter. On the base of this value, distance estimations can be performed. If beacons from other manufacturers or different power settings are used, a different tx-power might need to be provided to the SDK. This setting is global. Each beacon can also be configured manually in the dashboard, which will have higher priority. If no value is provided either way, it defaults to -65 dB.

  • Obstacle Checking
    Prevents the position moves through obstacles. Obstacles can be defined with different strengths, defining how many position-updates are necessary for a fallback to trigger. Is an advanced alternative to magnetic path snapping. Also, keep in mind that this feature only works if a KML (see Backspin web-frontend documentation) has been provided. Enabling this configuration without a KML will render effectless.

  • Magnetic Path-Snapping
    Keeps the position snapped to the path, considering prior updates. If there are two parallel paths near each other, this can avoid arbitrary jumps between them. Has been proven useful in installations that contain long corridors or supermarket isles. However, if the initial path which will be snapped to magnetically is false, the result is worse. The calculated position can also stick to corners/intersections at sharp turns if performed quickly. Works only if NavigationPathSnapping is enabled.

  • Magnetic Path-Snapping Fallback-Threshold
    The threshold defining the number updates the position will stay snapped. A low count results in faster changes but less magnetism.

  • Magnetic Path-Snapping Distance-Threshold
    Should represent the distance between two "parallel" paths. If they are farer apart then this threshold, magnetic path snapping will not have any effect. Can also be dynamically changed on runtime if the environment changes (e.g. a different level with paths farer apart). The default is 3m, which represents an average aisle-width in a supermarket.

Background Beacon Ranging

for beacon-ranging to work in the background for longer than 10 seconds (that are possible via beacon-monitoring), the app needs to have the location background-mode. The BackspinSDK will start "GPS" updates that will stop the app from being suspended.

If the background-mode is setup, the app will continue to range for beacons until the battery is empty, or the beacon-region is left. If there are cases where the user is not expected to leave the beacon-region (e.g. on a cruise-ship), the ranging would continue and drain the battery over time. You can manage the beacon-ranging either via stopping it, or via disabling the background-mode at some point.

// stop the positioning/ranging manually
BackspinSDKController.sharedInstance()
    .stopIndoorPositioning(
        ofBackspinPositioningBeaconsAndStopMonitoring: false)

// disable the background-mode, will stop GPS updates as well as beacon-ranging in background
BackspinSDKController.sharedInstance()
    .useBackgroundGpsUpdates = false

The latter has the advantage that beacon-ranging continues automatically once the app comes back to foreground.

You might setup a timer to stop the background locations after a certain time - maybe even configurable by the app user. For example if the user does not open the app for 1 hour, stop all background activity.

Crypto Beacons

The BackspinSDK supports the bluloc crypto feature. Since v3.6.0 it also supports mixed environments with non-crypto, V1, and V2 crypto configuration.

There is no need to configure anything (any more), the decryption of crypto-beacons happens automatically.

Some configurations might be required for the decryption to work, depending on the used crypto-version.

Crypto V1

With crypto V1 beacons, the beacons change their major and minor in regular intervals. Each major/minor combination of a beacon can be decrypted back to a single "beacon-number", which in turn can be a combination of a major from 0-15 and a minor from 0-65535. For that, a crypto seed is used, which acts like a password.

For V1 you only need a global crypto seed that is used to encrypt and decrypt all beacons. There is an restriction to 1 crypto seed per app. The SDK does not support multiple seeds for different beacons right now.

Set the crypto-seed BEFORE starting the positioning/beacon-ranging/monitoring like so:

BackspinSDKController.sharedInstance().setCryptoBeaconRollingIdSeed(0xffffffff)

You will receive the crypto-seed from your contact person at favendo.

Crypto V2

While the configuration and encryption/decryption of bluloc crypto V1 beacons is simple, there is one trade-off: Once the crypto-seed is known, one can decrypt a beacon as long as it is advertising.

With crypto V2 beacons, there are consecutive time-slots, where each time-slot has its own decryption key. A key can only be used for its designated time-slot. This allows for leasing beacons for certain time-periods. Unless the keys are renewed, the decryption will not work any more once the last key has expired.

The fetching of crypto V2 keys is done automatically by the SDK, there is no need to configure anything. The download is triggered while rootscope-data are downloaded, e.g. via triggerContentUpdateFromServer or requestModelDownloadOrUpdateForRestEndpoint:.

However, for simpliticy there are two "constraints":

  • The keys are downloaded automatically while a rootscope-data update is performed, but the SDK only downloads keys for the upcoming 30 days by default.

  • After a key-download, a new server-request is performed at earliest after 15 days (half of the default timespan of 30 days) following the last key-download.

Both of these constraint can be changed by triggering a key-download manually:

BackspinSDKController.sharedInstance()
            .requestCryptoV2BeaconKeys:20
                forceRefresh:true
                success:nil
                failure:nil)

The cache period is always half of the days parameter, in case forceRefresh is not set.

In this example, the keys are to be downloaded for 20 days. By using the forceRefresh parameter, the 10 days cache period is ignored and the key-update is performed right then - given that there is a connection to the server.

the server-api limits the download to a maximum of 30 days in advance.

Mapview Considerations

Supplementary to the set of stability filters provided by the SDK, there are some UI adjustments that can facilitate more satisfactory user experience.

  • Smooth marker updates
    Instead of abruptly setting the position marker to its new destination, an animated transition can help facilitate the impression of a continuous movement.

// Google Maps
CATransaction.begin()
CATransaction.setAnimationDuration(0.95)
self.positionMarker.position = newCalculatedPosition.coordinate
CATransaction.commit()


// Apple MapKit (http://stackoverflow.com/q/31089430)
UIView.animate(withDuration: 0.95, animations: {
    annotation.coordinate = newCalculatedPosition.coordinate
}) { (completed: Bool) in
    // finished
}

Position Marker as Blue Dot Position Marker with Accuracy Circle

  • Position marker size
    The blue dot used in Google and Apple Maps to display the current user position does not translate too well to indoor positioning, since the scale of the map is different. A dot with a radius of 30 centimeters leads to the impression, the accuracy for this position would be equally good, which is generally not the case. If the position-marker itself already has a radius of 3 meters however, the unjustified expectation of the user can be mitigated.

  • Display live accuracy circle
    The CLLocation objects returned from the positioning-framework contain a horizontalAccuracy value in meters which can be used for an accuracy circle. They are displayed around the position-marker, comparable to the ones in Google and Apple Maps.

See the mapview example with a blue dot marker and a bigger "accuracy" marker, which also has an accuracy circle around it.

Compass Heading / Bearing

The Backspin iOS SDK listens to compass heading updates by default. The results are published via global NSNotifications with the key kFAVCompassHeadingUpdateNotification (or NSNotification.Name.favCompassHeadingUpdate in Swift 3).

// listen to notification kFAVCompassHeadingUpdateNotification
NotificationCenter.default.addObserver(self, selector: #selector(MyClass.didUpdateHeadingValue(:_)), name: NSNotification.Name.favCompassHeadingUpdate, object: nil)

// ...

func didUpdateHeadingValue(_ headingNotification: Notification) {
    let trueNorth = headingNotification.userInfo![kFAVCompassHeadingValueTrueNorth] as! NSNumber
    let deviceBearingToNorth = fmod(360.0 + trueNorth.doubleValue), 360.0)

    // rotate map or do something with it
    mapMarker.rotation = deviceBearingToNorth
}

The delegate callback shouldDisplayHeadingCalibrationPopupForCompass determines the display of the calibration overlays for the compass. Note that the compass-bearing is only available on iPhone and iPad, not iPod Touch.

Navigation

To calculate a navigation-route, the BSSDKNavigationTask offers a range of functions, for example targeting of multiple locations (e.g. entrances) from which the nearest one will be selected/targeted, as well as directly calculating turn-by-turn directions for each navigation update.

Before calling runTask() on a navigation-task, you need to set its start-location. You can also init the task with useSdkUserLocation set to true, then the task will pull the last calulcated position automatically each time runTask() is called - until updateStartLocation() is called the first time.
navigationTask = BSSDKNavigationTask(possibleTargetLocations: myTargetLocationList)
guard let startLocation = BackspinSDKController.sharedInstance().lastCalculatedIndoorLocation else { return }
navigationTask.updateStartLocation(startLocation)
navigationTask.completionHandler = { (task, steps, destinationReached, destination) in
    // my completion code
}
navigationTask.runTask()
You need to keep a strong reference to the navigation-task, otherwise the completion-block will never be called.

The list of parameters:

  • startLocation
    The start point for the route, usually the calculated indoor-position.

  • isMainActiveNavigationTask
    If navigation-path-snapping is enabled during positioning, set this flag to save resources by re-using the snapping result. There is no additional input necessary, the data will be retrieved internally. If startLocation is not the calculated position from the BackspinSDK, setting this flag can have undefined results.

  • possibleTargetLocations

    • Can be used if a shop has multiple entrances (even on different levels). The framework will only return a route to the closest target. (NO traveling salesman problem)

    • During each route calculation, the shortest path to any of the locations is re-calculated, so the actual target might change.

    • If any of the locations is in proximity to the start point (on the same level and within navigationTargetProximityDistance meters), the navigation-task will be ended.

  • possibleDestinationVenueLocations and targetedVenueLocation
    As convenience it is also possible to input a set of BSGenericModelSQLite of type VenueLocation (the return-value of a venueModel.venueLocations()). The CLLocation objects will be extracted from each venue-location and the targetedVenueLocation will contain the targeted one. Again, only the nearest venue-location is targeted and not all in successive order!

  • lookForBarrierFreeRoute
    If barrier-freedom was defined in the KML (ask your favendo project manager), this flag can be used return barrier-free paths only. If no barrier-free route could be found, the returned set of route-steps is empty.

There is a global parameter navigationTargetProximityDistance (in meters) on the BackspinSDKController that can be used to specificy at which distance to the (nearest) target the navigation can end.

Dynamic Targets

Furthermore, the target-set can be updated on runtime of the task by calling updatePossibleTargetLocations(_) on a task object. This is useful if an asset-tracking object is to be used as target, which can change its position rather quickly.

There are some limitations on this:

  • If a route-calculation is performed at the same time, the target-update will be delayed until the next calculation is requested (meaning, until runTask is called the next time).

  • Furthermore, to save resources and keep the route-path-snapping consistent, the update-call will be ignored if the distance of the new target to the old target is less than the configured BackspinSDKController.sharedInstance().navigationTargetProximityDistance

in case there are multiple possible target locations, the two nearest locations will be compared against the proximity-distance parameter!

Favendo MapSDK

If used in combination with the Favendo MapSDK, the feature can still be utilised. Either by calling updatePossibleTargetLocations(:_) on the FAVIndoorMapview.activeNavigationTask (which is a BSSDKNavigationTask), or since v1.5.2 there is the convenience method FAVIndoorMapview.updateNavigationTargets(ofTask:_) which will forward the call, and also start a navigation if none has been started yet.

To use the navigation-feature of the BackspinSDK from the Favendo MapSDK, you need at least v1.6.0 of the MapSDK in combination with v3.6.0 of the BackspinSDK due to a minor API change. v1.6.0 works with older BackspinSDK versions but not vice versa.

User Account

Currently a user account is created automatically during the setup. In future versions it might also be possible to let the user login (and logout), thus allowing the user to change or share an account over multiple devices.

The user account handled by the Backspin iOS SDK currently has two features: An offer leaflet and a likes list. The offer-leaflet is used to manage gathered venue-offers (Backspin model type VenueOffer), while the likes-list can be used to like multiple models (e.g. Venue, VenueOffer, Beacon, LevelPlan).

If you want to use these features, you obviously need to work with the internal Backspin models, it is recommended to also take a deeper look at Model-Store & Server API.

There are a number of calls provided to handle the management of the leaflet and likes list:

You can retrieve all items from the leaflet and likes as dictionaries. Both have an "id" key with integers values. Likes have an additional "type" key with the model-type (Venue, VenueOffer, etc.) as possible values.

You may use the constants kBSModelKeyType and kBSModelKeyId.

- (nullable NSArray<NSDictionary *> *) getLikes;
- (nullable NSArray *) getLeaflet;
Both methods are also available with a "Models" suffix, returning arrays of the actual Venue or VenueOffer BSGenericModelSQLite objects.

To post an entire new likes array or leaflet with the above defined dictionaries:

- (void) updateLikes:(nonnull NSArray<NSDictionary *> *)likes;
- (void) updateLeaflet: (nonnull NSArray *)leaflet;

For convenience, you can simply toggle the status of a like item by passing a single dictionary with the two above defined keys or a leaflet item by passing its venue offer identification:

- (void) toggleLike: (nonnull NSDictionary *)like;
- (void) toggleLeafletItem:(NSInteger)leafletItemID;
The toggleLike method is also available with a "Models" suffix, taking a BSGenericModelSQLite object.

Profile Config

For each app a profile-config needs to be set up. Contact your favendo project manager to set up an account profile for your needs. Then each user can access her own profile via the SDK.

There are generic wrappers that offer manipulating the profile

- (void) requestAccountProfileFields:(BackspinSDKAccountProfileLoadedSuccessBlock _Nullable)completionBlock;
- (void) updateAccountProfileOnServer;

The first method will return an array of profile-fields, according to the created profile-config in the Backspin dashboard.

There are four types of fields supported at the moment:

  • BSSDKAccountProfile.domainField

  • BSSDKAccountProfile.freeTextSingleField

  • BSSDKAccountProfile.freeTextMultiField

  • BSSDKAccountProfile.freeTextRangeField

Notifications

APNS

iOS Notifications need to be allowed by the app-user notifications are possible:

[[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];

...

- (void) application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
    if(notificationSettings.types != UIUserNotificationTypeNone) {
        // register for remote notifications (you have to register an APNS certificate at developer.apple.com)
        [[UIApplication sharedApplication] registerForRemoteNotifications];
    }
}

The last call is not required for local notifications but is necessary for APNS pushes (Apple Push Notifications / App Account), which also requires the corresponding UIAppDelegate method to be implemented (if iOS < 10 is targeted)

/// optional, only if APNS pushes are used via Backspin
func application(_ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    if BackspinSDKController.sharedInstance().sdkState == .initialised {
    	// forward the token to the Backspin server to be able to receive timed pushes
    	// only AFTER the setup-completion-block has been called
    	BackspinSDKController.sharedInstance()
    		.registerDeviceWithApnsToken(atServer: deviceToken)
    }
}

Since APNS-token is registered during setup, it is recommended to save said APNS-token received from Apple above and hand over when the setup is performed:

// ...

/// optional, only if APNS pushes are used via Backspin
var apnsData: Data?

// ...

/// optional, only if APNS pushes are used via Backspin
func application(_ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    self.apnsData = deviceToken
}

// ...

	// perform the setup with the apns-token
	BackspinSDKController.sharedInstance().setupBackspin(
    	withBasicAuth: "YOUR_BASIC_AUTH_KEY",
    	forServer: .PROD,
    	andApnsTokenData: self.apnsData,
    	success: {
    		....

Hint: Make sure to also implement the error-callback in the application-delegate for debugging purposes.

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    NSLog("AppDelegate: Could not register for remote notifications: \(error)")
}

Notifications and Positioning

For all internal notifications (location-based-content and beacon-proximity-notifications), beacon-ranging is required. If neither WhenInUse nor AlwaysUsage location-services permissions are granted by the user, then obviously the SDK cannot produce these notifications.

If AlwaysUsage is granted, then the notifications-module will start monitoring for proximity notifications automatically (or rather the UUID of linked proximity-beacons). For location-based notifications, positioning needs to be triggered explicitly. See also die Positioning chapter of this documentation.

Notification Delegate

To receive notifications from Backspin SDK, implement the corresponding delegate method:

- (void)handleAppNotification:(BSAppNotificationModel *)appNotificationModel {
    ...
}

This delegate method uses the BSAppNotificationModel with the following properties (Source comments omitted here, please refer to the public header file):

@interface BSAppNotificationModel : NSObject
@property (readonly) NSString *notificationTitle NS_AVAILABLE_IOS(8_0);
@property (readonly) NSString *notificationBody;
@property (readonly) NSArray *actions;
@property (readonly) NSArray *notificationKeyValueData;

/** the linked notification-config model. See BSModelNotificationConfig.h */
@property (readonly) BSGenericModelSQLite *notificationModel;

/** number of times this notification has been executed (including the current execution) */
@property (readonly) NSInteger executionCount;
@end

The notificationKeyValueData property contains all the custom key-value pairs that were defined for this notification in the Backspin Dashboard, for instance:

[
	{"key": "foo1", "value": "bar1"},
	{"key": "foo2", "value": "bar2"},
	{"key": "foo3", "value": "bar3"}
]

The notificationModel links the notification-config which has more information about the notification.

While the notificationModel contains all beacons / zones that are linked, there is currently no information which beacon/zone actually triggered the notification.

BackspinSDKControllerDatasource

The Backspin iOS SDK automatically searches for the Localizable.string file to localise any popups. If you don’t use the Localizable.string file you should implement the BackspinSDKControllerDatasource protocol and return your own custom translations.

The following keys exist and should be translated:

locationmanager_ls_disabled_title locationmanager_ls_disabled_text

Notify the app-user that location-services are disabled

locationmanager_ls_unauthorised_title locationmanager_ls_unauthorised_text

Notify the app-user that the app is not authorised to use location-services

locationmanager_popup_ok

Text for the “OK” button of any popup with text as above

locationmanager_popup_gotosettings

Text for a button to go directly to location-services-settings

core_update_nointernetconnection_title core_update_nointernetconnection_text

Alert-View that no internet connection is available on first start and at subsequent starts until an initial internet connection has been established

core_update_longtimenoupdate_title core_update_longtimenoupdate_text

Alert-View that the last content-update was over two weeks ago

core_update_close

“Close” button for the two alert-views above

Without a Localizable.strings, implement the datasource method. There are several constants that can be used for the noted keys above:

// from BackspinSDKDelegate.h
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationLocationManagerServicesDisabledTitle;
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationLocationManagerServicesDisabledText;
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationLocationManagerServicesUnauthorizedTitle;
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationLocationManagerServicesUnauthorizedText;
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationLocationManagerPopupOk;
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationLocationManagerPopupGoToSettings;
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationCoreUpdateNoInternetTitle;
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationCoreUpdateNoInternetText;
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationCoreUpdateLongTimeWithoutUpdateTitle;
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationCoreUpdateLongTimeWithoutUpdateText;
FOUNDATION_EXPORT NSString *const _Nonnull kBSLocalizationCoreUpdateClose;
func localisedString(for localisationKey: String) -> String {
    var localisedString = localisationKey

    if localisationKey == kBSLocalizationLocationManagerServicesDisabledText {
    	localisedString = "Sijaintipalvelut poistetaan käytöstä"
    }
    else if localisationKey == kBSLocalizationLocationManagerServicesUnauthorizedTitle {
    	// ...
    }
    // ...

    return localisedString
}

func currentLanguageDesignator() -> String {
	return "fi"
}

Model-Store & Server API

As described in List of Functions, the Backspin iOS SDK contains an API to download, parse, and cache models. The following are some further remarks to help you using the API and make design-choices.

In general, the model-store as well as the models are read-only, so in case it is required to change a model locally, it would be best to use your own solution.

Model Request

A short example on how the workflow should look like:

// will download all internal required models (for positioning, navigation, notifications)
BackspinSDKController.sharedInstance().requestRootModelTypesDownloadOrUpdate([
            // additionally, download venues and venue-categories
            BSGenericModelSQLite.modelTypeVenue(),
            BSGenericModelSQLite.modelTypeVenueCategory()
        ], language: "de",
        completionBlock: { (endPoint: BSBackendConnectionDownloadRestEndpoint, lastServerUpdate: Date) in

            // all venues from the current root-scope
            let venuesInCurrentRootScope = BSSDKModelStore.models(
            	forType: BSGenericModelSQLite.modelTypeVenue(),
            	withFilters: nil,
            	andRootScopeId: kBSSDKModelStoreUseCurrentRootScopeId)

            // all venues with a custom filter
            let shopsInCurrentRootScope = BSSDKModelStore.models(
            	forType: BSGenericModelSQLite.modelTypeVenue(),
            	withFilters: [
            		// only load shops
            		[kBSSDKModelStoreName: "venueType", kBSSDKModelStoreEquals: "shop"],
            		// only shops that DO NOT have "kiosk" in its name
            		[kBSSDKModelStoreName: "name", kBSSDKModelStoreLike: "kiosk", kBSSDKModelStoreNot: true]
            	],
            	andRootScopeId: kBSSDKModelStoreUseCurrentRootScopeId)
        }
There is a cache within the SDK, that prevents re-download of models more often than 15 minutes apart. That comes additional to potential server-side caches of the REST-results. At least the 15 minutes can be circumvented by resetting the timestamps and forcing a server-request. However, that is not recommended for the default-download, as this will request all data from the server again. So only use that method if the user wants a force-update of the app-data, e.g. via some pull-to-refresh action or during "debugging"/testing.

Here is an example how to force a content update right away:

let wholeContentRequest = BSBackendConnectionDownloadRestEndpoint.endPointWithLanguage("de", andRootScopeId: kBSSDKModelStoreUseCurrentRootScopeId)

var requestOois: [BSBackendConnectionDownloadObjectOfInterest] = []
for ooi in [BSGenericModelSQLite.modelTypeVenue(), BSGenericModelSQLite.modelTypeVenueOffer(), BSGenericModelSQLite.modelTypeVenueCategory()] {
    let thisOoi = BSBackendConnectionDownloadObjectOfInterest()
    thisOoi.rootModelDTO = ooi

    requestOois.append(thisOoi)
}
wholeContentRequest.objectsOfInterest = requestOois
            BackspinSDKController.sharedInstance().resetServerUpdateTimestampsAndTriggerModelDownloadOrUpdate(for: wholeContentRequest, andCompletionBlock: { (restEndpoint: BSBackendConnectionDownloadRestEndpoint, lastServerUpdate: Date) in
    // the data has been updated
})

Objects of Interest

The REST v2 API is designed to be less "chatty", thus reducing the amount of REST calls for each client. Instead of separate endpoints for each model-type, there is one global endpoint for all models. Since not all model-types might be required, the amount of data downloaded can be reduced with exclusion filters or a positive list of "objects of interest".

To reduce the number of requests, the SDK itself will not perform any updates on its own. It is advisable to call triggerContentUpdateFromServer inside the completion-block of the initial setup, which will download the minimum set of model-types:

  • Beacon

  • Level

  • NavigationGraph

  • NotificationConfig

Additionally, these (root) model-types can be requested. For each one of these, a set of exclusions can be defined for unwanted properties (at the root-level)

  • Venue

  • VenueCategory

  • VenueOffer

Furthermore, if a suitable user-token is set, asset-tracking models can be downloaded as well, but with a separate request, more about that in the next section.

For each root-venue / root-scope only ONE request needs to be sent. All models for a specific root-scope are saved in one language at a time only. If the language for a root-scope request changes or anything in the objectsOfInterest set is different, all models are downloaded again.

To complete the source-example from the prior section to request an extended set of model-types:

BSBackendConnectionDownloadRestEndpoint *restEndpoint =
[BSBackendConnectionDownloadRestEndpoint endPointWithLanguage:@"de" andRootScopeId:23];

BSBackendConnectionDownloadObjectOfInterest *objectOfInterest = [BSBackendConnectionDownloadObjectOfInterest new];
objectOfInterest.rootModelDTO = [BSGenericModelSQLite modelTypeVenue];
// do not include the "venueLocations" property for a venue-model
objectOfInterest.propertiesToBeExcluded = @[@"venueLocations"];

restEndpoint.objectsOfInterest = @[objectOfInterest];
[[BackspinSDKController sharedInstance] requestModelDownloadOrUpdateForRestEndpoint:restEndpoint andCompletionBlock:nil];

In this request, besides Beacon, Level, NavigationGraph, and NotificationConfig model-types, all Venue models will be downloaded but without their venueLocations property. All models would be downloaded in German (de) and for the root-venue with scope-id 23.

You may reuse the objectsOfInterest set for other rootscope requests as well.

If you require a custom model-type to be loaded/updated, you must use requestModelDownloadOrUpdateForRestEndpoint every time rather than triggerContentUpdateFromServer.

One remark: While it is possible to specifiy excluded properties for each root-model-type, also the "internal" ones like Beacon, NavigationGraph, etc. it should be handled with caution. If a property is excluded that is required, then the SDK might not work as expected any more. You may consult with favendo if you can safely exclude a specific property on an "internal" model-type. One property you can safely exclude for all model-types is modifiedAt which is not required on per-model-basis.

Model Store

All models downloaded from Backspin are persisted as JSON, so the only model-object that is available is BSGenericModelSQLite which offers generic accessors according to object-type (e.g. stringForName:). All nested/linked models will be replaced with generic-model-instances automatically, e.g. the linked VenueCategory models from within a Venue model:

#import <BackspinSDK/BSModelVenue.h>
#import <BackspinSDK/BSModelVenueCategory.h>

...

// get all venues which's type is "shop" (rather than "facility")
NSArray *venueModelsWithFilter = [BSSDKModelStore modelsForType:[BSGenericModelSQLite modelTypeVenue]
    withFilters:@[@{@"name": @"venueType", @"equals": kBSVenueTypeShop}]
    andRootScopeId:kBSSDKModelStoreUseCurrentRootScopeId
];

BSGenericModelSQLite *shopModel = venueModelsWithFilter.firstObject;
NSString *shopName = [shopModel venueName];

// references models with Type "VenueCategory"
NSArray<BSGenericModelSQLite *> *shopCategories = [shopModel venueCategories];
BSGenericModelSQLite *firstShopCategory = shopCategories.firstObject;
NSString *shopCategoryImageUrl = [firstShopCategory venueCategoryRelativeImageUrl];

The SDK provides a set of Objective-C categories for BSGenericModelSQLite models returned by the rootscopedata REST endpoint:

  • BSModelAssetTracking.h

  • BSModelBeacon.h

  • BSModelLevel.h

  • BSModelNavigationGraph.h

  • BSModelNotificationConfig.h

  • BSModelVenue.h

  • BSModelVenueCategory.h

  • BSModelVenueOffer.h

In order to save client and server resources, an update-operation is only performed at most every 15 minutes. For an instant update-operation, call resetServerUpdateTimestampsAndTrigger
ModelDownloadOrUpdateForRestEndpoint:andCompletionBlock: While the update-operation is in progress, all existing models remain available.
An existing BSGenericModelSQLite obtained from the model-store will not change with an update-operation. If access to the newest data is imperative, reload all models once the BackspinSDKRestEndpointModelRequestCompletionBlock is called.

Download Images

Serveral of the models offer relative image paths, like venueRelativeImageUrl, venueCategoryRelativeLogoUrl, offerRelativeImageUrl.

The public API offers a method to retrieve images with arbitary resolutions:

let someOffer: BSGenericModelSQLite

BackspinSDKController.sharedInstance().requestImage(atRelativeServerRestPath: someOffer.offerRelativeImageUrl(), withMaximumDimensionInPixel: 1080) { [weak self] (image) in
     self?.myImageView.image = image
}

Asset Tracking

The SDK offers a polling API for asset-tracking updates, the API is documented in BackspinSDKController+AssetTracking.h

If authentication is required, you need to provide a user-token to the SDK before requesting any assets, as otherwise you might not have sufficient permissions to read asset-data, see setAssetTrackingUserToken:.

There are five model types for asset-tracking:

  • Asset
    The main model, this defines an asset that is tracked.

  • AssetPosition
    The position for a given asset. This is in world-coordinates (latitude/longitude) with a level-number and accuracy. It also references all zones that this position lies in.

  • AssetZone
    A geo-polygon in which an asset can be in. It has a name, level, and corners as latitude/longitude list.

  • AssetZoneAlarm
    A zone-alarm object defines when to trigger a zone-alert for an asset, i.e. this is the "configuration" for future alerts. It defines a list of zones and if an alert should be triggered if the asset enters or exists these zones. Each zone-alarm can be activated or deactived.

  • AssetZoneAlert An alert is triggered if an asset enters or exists a zone that is defined in a zone-alarm.

Furthermore, the API has (up to) three parts for each request: Performing an asynchronous server-request, updating information for existing data, and retrieving cached data from the local model-store.

For example for assets there are three methods:

  • loadAssetTrackingAssetsAsynchronously
    This will perform a server-request to load asset-information (including their asset-positions). There is no caching involved, each method-call will result in a http-request to the server.

  • updateAssetTrackingAssetPositionsForIds
    After the first method has been called, this method can be used to just update the asset-position, since the asset-information is unlikely to change. Also, a set of asset-IDs can be defined that are to be updated. This method should be used for background updates, as there will be less data downloaded compared to loadAssetTrackingAssetsAsynchronously

  • getAssetTrackingAssets
    This call will retrieve existing models from the local modelstore. If no assets have been downloaded yet, this will return an empty list.

There are also two methods to help retrieving assets that are identified by an external ID:

  • loadAssetTrackingAssetsAsynchonouslyByExternalIds:

  • getAssetByExternalId:

There will return/retrieve asset-models. For further handling, use their respective modelID (which is different to their externalId property).

Continuous Updates

One can register for continuous updates for asset-positions and zone-alerts. There is a simple API with a block/closure that is called whenever an update is received from the Backspin server:

  • startWatchingAssetPositions:
    Start watching asset positions. The block is called whenever an asset-update is received.

  • stopWatchingAssetPositions
    Stop watching asset positions.

  • startWatchingAssetZoneAlerts:
    Start watching asset zone alerts. The block is called whenever an alert is triggered in the backend.

  • stopWatchingAssetZoneAlerts Stop watching asset zone alerts.

Websocket Status

A block can be set that is called each time the websocket status changes, e.g. to display information for the user that live-updates are interrupted for asset-positions:

[BackspinSDKController sharedInstance].websocketStatusUpdateBlock = ^(BSSDKWebsocketStatus websocketStatusUpdateCode) {
    if(websocketStatusUpdateCode != BSSDKWebsocketStatusConnected) {
        // print an info-message for the user
    }
};

The status can also be retrieved via [BackspinSDKController sharedInstance].websocketStatus

Analytics

A list of the out of the box analytics event types can be found at Analytics. Additionally, custom analytics events can be queued and sent to the server.

For custom events, the BSSDKAnalyticsTypeHandler protocol is to be implemented, which modifies the data to an analytics-event.

Add your implementation to the set of handlers via [[BackspinSDKController sharedInstance] addAnalyticsTypeHandlerIfNotAlreadyAdded:] The handler will be called for every analytics-event that is about to be sent to the Backspin server, so make sure to check for the currentEventType if it is relevant for you.

By default, an event has the following values:

{
    "type": "ANALYTICS_EVENT_TYPE",
    "timestamp": 1441283129969,
    "timezone": "Europe/Berlin",
    "timeoffset": "7200",
    "platform": {
        "device": "iPod Touch 6th Gen",
        "platform": "iPhone OS",
        "platformVersion": "8.4.1"
    },
    "payload":
    {
        "rootScopeId": ROOTSCOPE,
        "appAccountId": HASHED_APP_ACCOUNT_ID,
        "userAccountId": HASHED_USER_ACCOUNT_ID
    }
}

The rootScopeId field will not be added, if no rootvenue/rootscope has been selected (e.g. if the SDK was setup via setupBackspinWithoutRootVenueSelectionWithBasicAuth).

The account-ids will be hashed, if enableAnalyticsPrivacyMode is set. Otherwise the plain app-account-id and user-account-id will be used.

All other values need to be provided by the BSSDKAnalyticsTypeHandler. You should put your data in payload.

This is an example of an internal analytics-manager for indoor-location events:

@implementation BSLocationAnalyticsManager

- (BOOL) respondsToEventType:(NSString *)currentEventType {
    return [currentEventType isEqualToString:@"location_updated"];
}

- (void) addFieldsToJsonObject:(NSMutableDictionary *)jsonObject forEventType:(NSString *)currentEventType andData:(NSDictionary *)analyticsData {
    jsonObject[@"location"] = @{
        kFAVLatitude: analyticsData[kFAVLatitude],
        kFAVLongitude: analyticsData[kFAVLongitude]
    };
    jsonObject[@"payload"] = @{@"level": analyticsData[@"level"], @"isIndoors": analyticsData[@"isIndoors"]};
}

@end

You can put any information into the payload. That additional data might not show up in the Backspin analytics-dashboard, however.

If your custom analytics-event is related to an indoor-location, you might want to add the location-key with the latitude and longitude to your generated JSON object, as well as the keys level and isIndoors in the payload. The current root-scope-id, app-account, and user-account are added automatically. By default the app-account and user-account are hashed to protect the users privacy.
Internally each analytics-handler has to be a unique class. If you want to handle to different analytics-events, perform "analytics-type"-checking internally and add only 1 instance of your BSSDKAnalyticsTypeHandler or create two different classes. Otherwise only the first instance of a class will be added as handler!

Subscriptions

Before using subscribe to anything, call the setUser to provide the token.

Use the subscribeToEvent: method and choose the BSSDKEventType according to your need.

id<BSSDKSubscription> subscription = [[BackspinSDKController sharedInstance] subscribeToEvent:BSSDKEventTypeTrackingEvent forScope:[BSSDKEventScopes userWith:@"user~12345"] updateBlock:^(id<BSSDKBaseEvent> event) {
//This Block will be called everytime there is an update.
}];

The above code snippet shows the registration for position updates. There is also an BSSDKEventScope you have to pass. In that case we only subscribe to events of the trackable with the id user~12345. There is also a BSSDKEventScopes.all method which can be use to subscribe for all trackable position updates.

With a reference to the created subscription you are able to close it later.

[subscription close];

But you can also use the following method to close them all.

[[BackspinSDKController sharedInstance] closeAllSubscriptions];

Event Types

  • BSSDKTrackingEvent This event provides information about the position of an observed trackable. So when an asset or another users position gets updated you will receive this kind of event. It contains a nullable position and in addition a BSSDKTrackingState which tells you if the position is available or not. If not it could either be a permission change or the trackable was not reached anymore.

  • BSSDKConnectionStateEvent Subscriptions establish a two way connection to the Backspin backend to transfer the data. If you are interested in connection state changes on that underlying connection you can register for that event and see if the connection is active or not. The BSSDKEventScope parameter is ignored for that event type.

Troubleshooting / FAQ

How do I use a different server than PROD/UAT?

If you are not using PROD or UAT, you may also use a custom-server-url. For that, you have to import the module for "extended servers":

  • access in Swift via import BackspinSDK.ExtendedServers

  • access in ObjC via #import <BackspinSDK/BackspinSDKController+ExtendedServers.h>

import BackspinSDK.ExtendedServers

BackspinSDKController.sharedInstance().setupBackspin(
    withBasicAuth: "YOUR_BASIC_AUTH_KEY",
    forServerUrl: "https://custom-client.commander.favendo.com:443/backspin-backend/",
    success: {
        // logic in here
    })

How to download images for venues/offers/categories?

For all models there are relative-image-urls / logo-urls available. These serve as input for the public API, at which the type (JPG or PNG) as well as the resolution can be defined as well.

See also here for more details:

dyld: Library not loaded / libswiftXXX.dylib

If you cannot compile the project, some additional flags might need to be set. First, make sure all flags are set as described in Adjust Project Settings.

dyld: Library not loaded: @rpath/libswiftXXX.dylib
This error appears directly after start of the app:
dyld: Library not loaded: @rpath/libswiftXXX.dylib

If this error shows up after starting the app, make sure that the Build-Setting flag “Always Embed Swift Standard Libraries” is set to “Yes”.

In case there is no Swift code in your app, Xcode might not copy the Swift dependencies to the app, even if the build-setting above is set. If the error above still occurs, you might need to add an empty Swift-class to your app in order to enforce the copying of the Swift dependencies.

Apple App-Store Review

There are some questions that can be expected to be asked from Apple. This is a guideline for answers, that should also be provided proactively as App Review Information notes during the submission.

iBeacons

If Apple detects that the app contains code (such as the BackspinSDK) that is using iBeacons (via the CLLocationManager), they will likely ask the following questions:

 — Does this app detect startMonitoringForRegion:, startRangingBeaconsInRegion:, or both?

 — What is the user experience when the app detects the presence of a beacon?

It depends on the actual handling of the calculated indoor-position and evaluation of notifications, but for our showcase apps there is a boiler-plate answer that was sufficient so far (in respect to the features of the showcase apps)

This app uses iBeacon-ranging and monitoring as showcase for indoor-positioning and proximity-marketing via local beacon-triggered notifications. Monitoring is used to wake the app up, and then beacon-ranging is used to calculate an indoor-position.

Why are we using BLE?
We (Favendo) have a new technique that uses iBeacon signals to calculate a virtual indoor geoposition plus the corresponding floor level. We install iBeacons in big venues (shopping centres, train station, airports) of our clients, draw indoor maps and link the real position of the beacons to these maps. To show the users actual position on a map we use location services like GPS and WiFi for outdoor positioning, and as soon as we have the signals, the indoor position is calculated via iBeacon signals.

Location Background Mode

If the app uses a location-background mode (see also Notes about iOS Background Modes) to continue beacon-ranging (and therefore, positioning and proximity stuff) while in background, one can expect more questions and more scrutiny from Apple as to why this is necessary.

The important thing is to make it clear why the app-user has a significantly improved user-experience as if the app uses location just in foreground.
If location-background-mode is enabled for an App, as long as "Always" location-authorization is given, the app will stay active as long as "GPS" updates are listened to. It does not matter if GPS, GLONASS, Cellular, or WiFi is available and actual updates are coming in, the app does not get suspended unless the GPS updates are stopped. This is useful, as it allows to range beacons even in environments that do not get any other signals than from beacons (e.g. in underground parking garages).
Obviously this can also be abused to just keep the app running, but as the user has no possibility to stop this behaviour other than disabling location-services, it is also a critical privacy-intrusion, which is why Apple has an eye out for it.

 — What is the purpose of declaring location background mode? Please explain the need for this background mode and where the usage can be found in your binary.

 — If this app uses 3rd party SDKs for iBeacons, please provide links to their documentation showing that background location is required for it to function.

 — What features in this app use background location?

One thing Apple wants to hear is that the app does stop using background-location once it is not required any more. In the BackspinSDK the background-location is only active as long as user is in a beacon-region and is disabled once the region is exited.

This app uses iBeacon-ranging and monitoring as showcase for indoor-positioning and proximity-marketing via local beacon-triggered notifications. The monitoring is used to wake up the app on entering the exhibit-region; it then uses the location-background-mode in order to keep ranging beacons while at the exhibit. The location-updates and beacon-ranging are both stopped as soon as the monitoring detects a beacon-region-exit event.

It is also recommended to put the extended text in here that is listed at Notes about iOS Background Modes.

One thing that is required and might lead to an app-rejection if left out, is a warning-text at the end of the app-description about GPS-usage. While you can translate it to each supported language, it is highly recommended to keep the original in English as well for the review process.

Continued use of GPS running in the background can dramatically decrease battery life.

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

If an app with the BackspinSDK (or MapSDK) 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 MapSDK (FavendoIndoorMapview.framework and FavendoMapEngine.framework), if used.

Can the SDK size be reduced?

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/Backspin-iOS-SDK/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

Can I receive SDK notifications and position updates in the background?

Since BackspinSDK v3.1.0 the background-ranging capabilities have been extended, to continue ranging for beacons while the app is not in foreground-mode. For that, the Location background-mode is used that needs to be set for the target app in Xcode.

Once a beacon-region is entered, "GPS" updates are started, as long as the app receives "GPS" updates, the SDK also continues to range beacons. If the last beacon-region is exited, the GPS-updates are also stopped, to conserve battery and to comply to Apple’s App Store Review Guidelines.

Depending on the use-case it might be a good idea to reduce the ranging-frequency in the background, or set a positioning-configuration with less computational overhead (e.g. the PowerSaving accuracy-configuration). If in the background only beacon-proximity-notifications are needed and no positioning, it is also possible to disable positioning alltogether in the background.

See the next two sections for requirements about the background-mode.

Notes about iOS Background Modes

During the review process, Apple might take a second look if an app requires any kind of background activity as indicated by settings in the apps Info.plist.

Since iOS 9, suspended apps that want to work with the CLLocationManager in any way need a "Location Updates" background mode flag, which is a permission for GPS as well as beacon updates. However, even if an app uses beacon related activity in background-mode, Apple wants to make sure this use is justified. As generally the case in App Store Reviews, it will be Apple deciding whether your reasons meet their standards of justifiability.

Remember that even without the "Location Updates" background mode, the app will be woken up with monitoring and can perform tasks for 10 seconds. Proximity events triggered by smartly placed beacons usually suffice.

Apple does allow background mode for "Navigation" apps which provide continuous turn-by-turn directions in the background. Such apps usually provide speech output for the next direction while in background.

However, in our findings Apple does also allow the usage of location background mode if it is restricted to certain areas for user indoor positioning/navigation. If the Location background-mode is enabled for the app, the SDK will take advantage of "GPS" location updates to also continue ranging for beacons in the background, therefore providing the SDK-user with extended background-time. In our findings the beacon-ranging should continue indefinitely.

Please feel free to use this text for the submission form when you first submit your app / changes to the app with the sdk, or as an response to an Apple review question regarding location background mode:

Why are we using BLE?
We (Favendo) have a new technique that uses iBeacon signals to calculate a virtual indoor geoposition plus the corresponding floor level. We install iBeacons in big venues (shopping centres, train station, airports) of our clients, draw indoor maps and link the real position of the beacons to these maps. To show the users actual position on a map we use location services like GPS and WiFi for outdoor positioning, and as soon as we have the signals, the indoor position is calculated via iBeacon signals.

Is our app in compliance with App Store Review Guidelines?
Yes, we (Favendo) let this review multiple times and discussed this with the App Review Team and an Apple representative. We’re using the BLE technique to calculate the position of a customer and send her local notifications within the shopping centre and only there. As we’re using virtual geopositions plus the floor level, which isn’t possible by regular GPS and WiFi, and due to the restriction of (iBeacon) region monitoring being set to 20 regions per App in background, we’re technically forced to use background location services: "2.16 - Multitasking Apps may only use background services for […​] location, […​] local notifications, etc."

Why do you use background services and not iBeacon region monitoring
As explained above, we (Favendo) are installing the hardware in big venues with more then 300 iBeacons on a region of 200 x 200m with multiple floors. We’re not installing the iBeacons per retailer, we’re installing the iBeacons on the walls of the floors and then calculate a virtual geoposition with latitude and longitude plus a floor level. This makes it possible to notify a customer in front of a store - if she finds something interesting there - with a local notification. The restriction of the region monitoring for iBeacons to 20 regions per app and the inaccurate resoultion of the position makes it necessary to use background services to have a calculated geoposition, to notify the customer even when she has the app in background. We’re using backgound services only in the region of the shopping centre: when the customer enters the shopping centre we notify her via significant location change and ask her, if she want’s to use the feature. If she leaves the area, it will be switched off automatically. This obviously saves the battery life. This feature has been approved by Apple before explaining it via phone call with Richard Chapman from Apple, CA.

Do I really need "AlwaysUsage"? This might scare app-users.

Apple wants to make sure that the privacy of app-end-users is a high priority, while at the same time use battery power as conservatively as possible. Any kind of background activity, especially in combination with the users location, is not taken lightly - which is one reason why the AlertView text sounds so "scaring".

Allow "App X" to access your location even when you are not using the app?

Unfortunately, beacon monitoring does not work - neither in background nor in foreground - if the AlwaysUsage permission is not granted. Positioning and beacon ranging can still be started "manually" though.

You do not need the AlwaysUsage permission if you do not perform anything in the background - as described in detail above. But if AlwaysUsage is enabled and granted by the user, even without Background Mode, it is still possible to wake up the app for about 10 seconds even by monitoring. If you do not intend to publish a location-based or beacon-proximity notification while the app is in background (or any notification that is related to entering a beacon-region), the WhenInUse permission suffices. Likewise, if you merely display a map with a user’s position and stop any Beacon-related activities if the map is not visible, neither a Background Mode nor the AlwaysUsage permissions are required.

Positioning / Beacon ranging does not start

Make sure that Bluetooth is enabled on the device and a permission has been granted for Location Services, either AlwaysAndWhenInUseUsage (AlwaysUsage for iOS < 11) or WhenInUse. Also, take a look at Beware of Airplane / Flight Mode

If the user has just revoked the permission or disabled Bluetooth, the monitoring will start (again) as soon as the AlwaysUsage permission is given again and Bluetooth is enabled.

With the WhenInUse permissions, the positioning/ranging has to be started "manually":

// start positioning/ranging NOW, regardless if beacons are around or not
// only requires the "WhenInUse" permission
[[BackspinSDKController sharedInstance] startIndoorPositioningWithBackspinPositioningBeacons];

In this case positioning will stop once the app goes in to background mode.

It is also possible that your data is not available yet. Since Backspin iOS SDK version 2.0 no data is downloaded automatically, you will need to call triggerContentUpdateFromServer or requestModelDownloadOrUpdateForRestEndpoint: before positioning, navigation, or notifications can be used. See also Model-Store & Server API for the API to request models from the server.

Make sure that the beacons are configured for "Navigation" in the dashboard and the correct root-scope is selected. A useful indicator for incorrect data is to check whether beacons are ranged at all (e.g. if the global named kFAVBeaconsRangedNotification notification is fired).

See also The setup takes too long if you can see ranged beacons, yet still do not receive notifications or a position.

You can also analyse ranged beacons to see if they are configured correctly:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(rangedNewSetOfBeacons:) name:kFAVBeaconsRangedNotification object:nil];


- (void) rangedNewSetOfBeacons:(NSNotification *)beaconRangedNotification {
    NSDictionary<NSString *, NSArray<id<FAVBeaconRangeResult>>> *beaconData = beaconRangedNotification.userInfo[kFAVBeaconRangedNotificationUserInfoSmoothedBeacons];

    for(NSString *uuid in beaconData) {
        NSArray<id<FAVBeaconRangeResult>> *beaconsForThisUuid = beaconData[uuid];
        // further analysing of these beacons...
    }
}

Beware of Airplane / Flight Mode

While regular beacon-ranging works if bluetooth is enabled and location-services authorization has been granted, beacon-monitoring will not work on airplane mode.

First, monitoring should be started while the device is not in airplane mode. Unfortunately there is no straight forward way to detect if the device is in airplane mode or not (there are ways to check connectivity, which does not mean that airplane mode is active).

Second, while airplane mode is enabled, no beacon-region events are triggered, even if Bluetooth is enabled.

Third, one strange phenomenon: If the device is inside a beacon-region when airplane-mode is enabled, it will still be in this region once it is disabled again, even if that might be hours later - therefore, no new beacon-region-enter event will be fired by iOS, not waking up the app if it is in the background. While there is functionality in the SDK to cover the case that the device is already inside the region, that logic only works if the app is in foreground and can execute code. In background-mode the app/SDK can only rely on enter (or exit) events that are fired by iOS.

No valid navigation route is returned

It is possible that your data is not available yet. Since with Backspin iOS SDK version 2.0, no data is downloaded automatically, you will need to call triggerContentUpdateFromServer or requestModelDownloadOrUpdateForRestEndpoint: before positioning, navigation, or notifications can be used. See also Model-Store & Server API for the API to request models from the server. See also the next section for timeouts.

Make sure that a valid path actually exists. If you defined a KML with directed paths, there should be at least one route in the desired direction that is passable. Likewise, if barrier-free navigation is enabled, there should be at least one barrier-free path.

The setup takes too long

There are multiple reasons why the setup-block is not called or with a high delay. Usually, either the Backspin server is offline/not reachable or the device has no sufficient internet connection.

If this is the first time the setup-method is called, the SDK will render functionless, since it needs to initially register at the server and download data. The SDK will wait until it finds the Backspin server reachable (see Apple Reachability for iOS). Only if the reachability status changes, an request-attempt is made. After the initial download, all data is available offline. If the reachability status indicates that the Backspin server is reachable, an update request might be attempted.

If cellular data is available but no data can be received (e.g. exhausted mobile data plan), iOS will determine the device to be online and the SDK will try to perform data updates if the SDK-data has "expired", so the SDK callbacks are called with a delay until the requests time-out. Custom timeouts can be set in order for the update-requests to terminate faster. There are three configurable network timeouts in the SDK:

requestTimeoutForAccountRegistration An account is created at the very first start of the SDK and update whenever a new APNS-token is provided. If it is not the first start any more, the setup will return immediately and use offline-data.

Only after the account-request returns, the setup is continued. By default, the account-request times out after 10 seconds.

requestTimeoutForRootVenues After the account data has been loaded (from server or the offline storage), the root-venues are updated. An update is only performed every half an hour and only if the Backspin server reachability status is Reachable. Only if the setup will continue/complete, the root-venue-request returns. By default, the root-venue-request times out after 10 seconds.

requestTimeoutForOtherRequests Used for all other requests.

In the worst-case scenario where all consecutive timeouts are triggered, it can take up to 20 seconds until the setup is completed and another 10 seconds for the SDK is to start loading data (if prior setup was successful).

The data download takes too long

See the prior section about (default) timeouts that might need to be adapted if you expect the user to have slow internet connection or a ton of data (many beacons, venues, huge navigation-graph etc).

Another optimization that can be done is to segment the data-loading. Let the SDK load the basic model-types first that are needed for its base functionality (beacons, levels, navigation-graph) - this way you can already start display a mapview (e.g. the Favendo iOS Map) with positioning and navigation enabled. After that request has finished, you can make a second request downloading additional data (e.g. venues, venue-categories, …​)

Especially with a lot of venues, this increases the initial loading-time and responsiveness.

A simple example to implement the segmented loading:

extension Notification.Name {
    static let backspinLoaded = Notification.Name("BackspinModelsLoaded")
}

/// if you want to download everything from scratch, use forceRefresh to reset all timestamps to 1970-01-01
public func triggerLoadingBackspinModels(forceRefresh: Bool = false) {
    // first load all the essentials (see after block-definition) ...
    // ... and after completion download all the extended models to increase loading time
    let completionBlock = { (restEndpoint: BSBackendConnectionDownloadRestEndpoint, lastServerUpdate: Date) in
        self.triggerExtendedBackspinModels()
    }

    if forceRefresh {
        BackspinSDKController.sharedInstance()
            .resetServerUpdateTimestampsAndTriggerModelDownloadOrUpdate(for: prepareEndpoint(),
               andCompletionBlock: completionBlock)
    }
    else {
        BackspinSDKController.sharedInstance()
            .requestModelDownloadOrUpdate(for: prepareEndpoint(),
               andCompletionBlock: completionBlock)
    }
}

private func prepareEndpoint() -> BSBackendConnectionDownloadRestEndpoint {
    // add desired language and root-scope here, if needed
    return BSBackendConnectionDownloadRestEndpoint()
}

private func triggerExtendedBackspinModels() {
    // notify everyone that content has loaded, at the initially
    NotificationCenter.default.post(name: Notification.Name.backspinLoaded, object: nil)
    BackspinSDKController.sharedInstance().startMonitoringBackspinPositioningBeacons()

    let wholeContentRequest = prepareEndpoint()

    let venueOoi = BSBackendConnectionDownloadObjectOfInterest()
    venueOoi.rootModelDTO = BSGenericModelSQLite.modelTypeVenue()
    venueOoi.propertiesToBeExcluded = ["modifiedAt"]

    let venueCategoryOoi = BSBackendConnectionDownloadObjectOfInterest()
    venueCategoryOoi.rootModelDTO = BSGenericModelSQLite.modelTypeVenueCategory()
    venueCategoryOoi.propertiesToBeExcluded = ["modifiedAt"]

    let venueOfferOoi = BSBackendConnectionDownloadObjectOfInterest()
    venueOfferOoi.rootModelDTO = BSGenericModelSQLite.modelTypeVenueOffer()
    venueOfferOoi.propertiesToBeExcluded = ["modifiedAt"]

    wholeContentRequest.objectsOfInterest = [venueOoi, venueCategoryOoi, venueOfferOoi]

    // AFTER initial download, request the rest
    // NOTE: no reset-server-timestamp necessary, because otherwise EVERYTHING would be downloaded again,
    // including the "essentials" and all this segmented loading would be useless
    // with the above `resetServerUpdateTimestampsAndTriggerModelDownloadOrUpdate` ALL timestamps are reset already.
    BackspinSDKController.sharedInstance().requestModelDownloadOrUpdate(
        for: wholeContentRequest, andCompletionBlock: {
            (restEndpoint: BSBackendConnectionDownloadRestEndpoint, lastServerUpdate: Date) in
            // notify everyone again.
            // Might also use a different notification to mark "ALL content has loaded"
            NotificationCenter.default.post(name: Notification.Name.backspinLoaded, object: nil)
    })
}

Can the SDK be initialised without root-scope?

You may use the setup-method setupBackspinWithoutRootVenueSelectionWithBasicAuth (see BackspinSDKController+ExtendedServers.h) to setup the SDK with an "invalid" rootscope id. The functionality of the SDK is mostly restricted to queuing analytics-events, as other functionality requires available data (which is linked to a root-scope), like notifications, positioning, navigation.

It is still possible to set a root-scope after the setup:

...

// get a list of all root-venue ids
[[BackspinSDKController sharedInstance] availableRootScopesIds:^(NSArray<NSNumber *>* _Nonnull rootScopeIds) {
    NSNumber *firstRootScope = rootScopeIds.firstObject;
    [[BackspinSDKController sharedInstance] setRootScopeId:firstRootScope.integerValue];
}];

It is also possible to retrieve more data about the root-venues, if needed. The Root-Venue models can be retrieved from the database and iterated:

// check which root-scope should be loaded
NSArray *rootVenueList = [BSSDKModelStore modelsForType:[BSGenericModelSQLite modelTypeRootVenue] withFilters:nil andRootScopeId:-1];

for(BSGenericModelSQLite *rootVenue in rootVenueList) {
    // a root-venue is still a regular venue for the most part, so you may use everything from BSModelVenue.h
    if([[rootVenue venueName] isEqualToString:@"MyVenue"]) {
        [[BackspinSDKController sharedInstance] setRootScopeId:[rootVenue rootVenueOwnScopeId]]
    }
}

Can cached images be purged?

The SDK uses the iOS native’s [NSURLCache sharedURLCache], which can be setup as desired. See http://nshipster.com/nsurlcache/ for a detailled explanation. The NSURLSCache can be purged any time, there is no reliance on existing files (except images are not available offline any more, obviously).

If you use [BSSDKLevelPlanModel requestLevelPlanTilesFileWithCompletionBlock:] to download levelplan-tile-files, then the purging needs to be handled manually. Those files are written into the apps Cache folder at /Cache/levelplans/levelplan_XXXX.mbtiles

Tiles File Folder Structure

You may delete the whole folder if you want to purge the tiles-files:

NSArray *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask,YES);
NSString *documentsDirectory = [path objectAtIndex:0];
documentsDirectory = [documentsDirectory stringByAppendingPathComponent:@"/levelplans/"];