iOS Beacon Detection Snippet
Guide to getting started with the AreaMetrics beacon monitoring platform.
Add the AreaMetrics.swift file to your project
AreaMetrics.swift
//
// AreaMetrics.swift
// AreaMetricsSnippet v1.7
// Copyright © 2018 AreaMetrics. All rights reserved.
//
import Foundation
import UIKit
import CoreLocation
import AdSupport
class AreaMetrics: NSObject, CLLocationManagerDelegate {
// MARK: Singleton Declaration
static let sharedInstance = AreaMetrics()
override private init() {}
// MARK: Simple Properties
let locationManager = CLLocationManager()
let venueUUIDs: Set = ["6CA0C73C-F8EC-4687-9112-41DCB6F28879",
"50765CB7-D9EA-4E21-99A4-FA879613A492",
"9EB353A0-69B6-4947-B710-BAE643C8BCA5",
"74278BDA-B644-4520-8F0C-720EAF059935",
"5993A94C-7D97-4DF7-9ABF-E493BFD5D000",
"56DB0365-A001-4062-9E4D-499D3B8ECCF3",
"F1EABF09-E313-4FCD-80DF-67C779763888",
"B8FED863-9F1C-447C-8F82-DF0C2E067DEA",
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
"B9407F30-F5F8-466E-AFF9-25556B57FE6D",
"A8D278E4-35B5-44EF-9CAC-0F3FC511FE3E",
"F2356731-FBD4-4A10-8AA2-C89ADF48A98D",
"FDA50693-A4E2-4FB1-AFCF-C6EB07647825",
"F7826DA6-4FA2-4E98-8024-BC5B71E0893E",
"D77657C4-52A7-426F-B9D0-D71E10798C8A"]
let amUUIDs: Set = ["B9407F30-F5F8-466E-AFF9-25556B577272"]
let defaultSession = URLSession(configuration: .default)
var lastSentBeacons = [String:TimeInterval]()
var baseURL = "https://api.areametrics.com/v3/"
var amPubID = ""
let AM_BATCH = "AM_BATCH"
let AM_BATCH_LAST_SEND = "AM_BATCH_LAST_SEND"
let SNIPPET_VERSION = "1.7"
var currentlySendingBatch = false
// MARK: Lazy Stored Properties to eliminate repeat calculations
lazy var vendorId: String? = {
return UIDevice.current.identifierForVendor?.uuidString
}()
lazy var adId: String? = {
guard ASIdentifierManager.shared().isAdvertisingTrackingEnabled else {
return nil
}
return ASIdentifierManager.shared().advertisingIdentifier.uuidString
}()
lazy var locale: String? = {
return Locale.current.regionCode?.uppercased()
}()
lazy var gdprUser: Bool = {
let gdprCountries: Set = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "GB", "IS", "LI", "NO"]
if locale != nil && gdprCountries.contains(locale!) {
return true
}
return false
}()
lazy var deviceModel: String = {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
return identifier
}()
lazy var deviceAgent: String? = {
var webView = UIWebView(frame: CGRect.zero)
let agent = webView.stringByEvaluatingJavaScript(from: "navigator.userAgent")
return agent
}()
// MARK: - Initial Configuration
enum AMException: Error {
case runtimeError(String)
}
func startService(pubID: String) {
if !pubID.hasSuffix("00") {
print("AreaMetrics startService called with invalid Publisher ID! Snippet is not initialized.")
return
}
amPubID = pubID
locationManager.delegate = self
sendUserInit()
if CLLocationManager.authorizationStatus() == .authorizedAlways {
startMonitoring()
}
print("AreaMetrics Initialized Successfully!")
}
private func startMonitoring() {
if gdprUser {
stopMonitoring()
return
}
let uuids = amUUIDs.union(venueUUIDs)
for uuid in uuids {
let region = CLBeaconRegion(proximityUUID: UUID(uuidString: uuid)!, identifier: uuid)
region.notifyOnEntry = true
region.notifyEntryStateOnDisplay = true
locationManager.startMonitoring(for: region)
}
}
func stopMonitoring() {
let uuids = amUUIDs.union(venueUUIDs)
for uuid in uuids {
let region = CLBeaconRegion(proximityUUID: UUID(uuidString: uuid)!, identifier: uuid)
locationManager.stopMonitoring(for: region)
}
}
// MARK: - LocationManagerDelegate Implementation
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedAlways {
startMonitoring()
}
}
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
if region is CLBeaconRegion {
let beaconRegion = region as! CLBeaconRegion
locationManager.startRangingBeacons(in: beaconRegion)
}
}
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
if region is CLBeaconRegion {
let beaconRegion = region as! CLBeaconRegion
locationManager.stopRangingBeacons(in: beaconRegion)
}
}
func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
let nowTime = Date().timeIntervalSince1970
for beacon in beacons {
let prox = beacon.proximityUUID.uuidString
let space: TimeInterval = amUUIDs.contains(prox) ? 5 : 5
if let lastSentTime = lastSentBeacons[beaconId(beacon)] {
if nowTime - lastSentTime > space {
addBeaconToBatch(beacon)
}
} else {
addBeaconToBatch(beacon)
}
}
}
// MARK: - Caching and sending decision logic
private func addBeaconToBatch(_ beacon: CLBeacon) {
lastSentBeacons[beaconId(beacon)] = Date.init().timeIntervalSince1970
var beaconJSON: [String: Any] = [:]
beaconJSON["time"] = Int64(Date.init().timeIntervalSince1970)
beaconJSON["uuid"] = beacon.proximityUUID.uuidString
beaconJSON["major"] = beacon.major
beaconJSON["minor"] = beacon.minor
beaconJSON["rssi"] = beacon.rssi
beaconJSON["beacon_accuracy"] = beacon.accuracy
beaconJSON["proximity"] = beacon.proximity.rawValue
beaconJSON["btype"] = "ibeacon"
let prefs = UserDefaults.standard
var batch = prefs.array(forKey: AM_BATCH) ?? []
batch.append(beaconJSON)
let batchOverflow = batch.count - 100
if batchOverflow > 0 {
batch.removeFirst(batchOverflow)
}
prefs.set(batch, forKey: AM_BATCH)
checkWhetherBatchReadyAndSend()
}
private func checkWhetherBatchReadyAndSend() {
let prefs = UserDefaults.standard
let batch = prefs.array(forKey: AM_BATCH) ?? []
if batch.count > 0 {
let lastSend = prefs.double(forKey: AM_BATCH_LAST_SEND)
if Date.init().timeIntervalSince1970 - lastSend > 1800 {
sendBeaconBatchToServer(batch)
}
}
}
private func clearBeaconBatch() {
UserDefaults.standard.removeObject(forKey: AM_BATCH)
}
private func markBeaconBatchSent() {
let prefs = UserDefaults.standard
prefs.set(Date.init().timeIntervalSince1970, forKey: AM_BATCH_LAST_SEND)
}
// MARK: - Network Activity
private func getPostString(params:[String:Any]) -> String {
var data = [String]()
for(key, value) in params {
data.append(key + "=\(value)")
}
return data.map{String($0)}.joined(separator: "&")
}
private func sendBeaconBatchToServer(_ batch: Array<Any>) {
if currentlySendingBatch {
return
}
currentlySendingBatch = true
let url = URL(string: baseURL + "beacon_batch")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var items: [String: Any] = [:]
items["snippet_ver"] = SNIPPET_VERSION.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
items["pub_id"] = amPubID.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
items["os"] = "ios".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
items["os_ver"] = UIDevice.current.systemVersion.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
items["model"] = deviceModel.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
if vendorId != nil {
items["vendor_id"] = vendorId!.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
}
if locale != nil {
items["user_locale"] = locale!.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
}
if !gdprUser {
if adId != nil {
items["ad_id"] = adId!.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
}
if deviceAgent != nil {
items["agent"] = deviceAgent!.addingPercentEncoding(withAllowedCharacters: [])
}
}
do {
let bytes = try JSONSerialization.data(withJSONObject: batch, options: [])
items["batch"] = String(decoding: bytes, as: UTF8.self)
} catch {
items["batch"] = "[]"
}
let postString = getPostString(params: items)
request.httpBody = postString.data(using: .utf8)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let task = defaultSession.dataTask(with: request) { data, response, error in
if (error == nil) {
self.clearBeaconBatch()
self.markBeaconBatchSent()
}
self.currentlySendingBatch = false
}
task.resume()
}
private func sendUserInit() {
var items = [URLQueryItem]()
items.append(URLQueryItem(name: "snippet_ver", value: SNIPPET_VERSION))
items.append(URLQueryItem(name: "pub_id", value: amPubID))
items.append(URLQueryItem(name: "os", value: "ios"))
items.append(URLQueryItem(name: "os_ver", value: UIDevice.current.systemVersion))
items.append(URLQueryItem(name: "model", value: deviceModel))
if vendorId != nil {
items.append(URLQueryItem(name: "vendor_id", value: vendorId))
}
if locale != nil {
items.append(URLQueryItem(name: "locale", value: locale))
}
if !gdprUser && adId != nil {
items.append(URLQueryItem(name: "ad_id", value: adId))
}
var permission : String?
if CLLocationManager.locationServicesEnabled() {
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
permission = "not_determined"
case .restricted:
permission = "restricted"
case .denied:
permission = "denied"
case .authorizedAlways:
permission = "authorized"
case .authorizedWhenInUse:
permission = "when_in_use"
}
} else {
permission = "disabled_globally"
}
if permission != nil {
items.append(URLQueryItem(name: "loc_permission", value: permission))
}
var urlComponents = URLComponents(string: baseURL + "user_init")!
urlComponents.queryItems = items
let url = urlComponents.url!
let task = defaultSession.dataTask(with: url)
task.resume()
}
// MARK: - Utility Macros
private func beaconId(_ b: CLBeacon) -> String {
let id = b.proximityUUID.uuidString + "." + b.major.stringValue + "." + b.minor.stringValue
return id.lowercased()
}
}
Implement the call to startService
In your UIApplicationDelegate implementation, call startService on AreaMetrics from didFinishLaunchingWithOptions. Replace "PUBLISHER_ID" in the following example with your publisher ID:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
AreaMetrics.sharedInstance.startService(pubID: "PUBLISHER_ID")
return true
}
#import "AreaMetrics-Swift.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[AreaMetricsSDK sharedInstance] startService:"PUBLISHER_ID_HERE"];
return YES;
}
When launching your app, you should see printed in the debug console, "AreaMetrics Initialized Successfully!"
Ask the user for Location Permission
If your app is not already asking users for Location Always Allow permission, you will need to implement the following:
pageiOS Location PermissionsLast updated