iOS Beacon Detection Snippet

Last updated 2 months ago

Guide to getting started with the AreaMetrics beacon monitoring platform.

Add the AreaMetrics.swift file to your project

AreaMetrics.swift
//
// AreaMetrics.swift
// AreaMetricsSnippet v1.5
//
// Created by Ryo Tulman on 6/28/18.
// 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 = ["50765cb7-d9ea-4e21-99a4-fa879613a492",
"6ca0c73c-f8ec-4687-9112-41dcb6f28879",
"74278bda-b644-4520-8f0c-720eaf059935",
"25359ac6-acfe-8fe5-92c7-a70f234704aa",
"b8fed863-9f1c-447c-8f82-df0c2e067dea",
"fda50693-a4e2-4fb1-afcf-c6eb07647825",
"f2356731-fbd4-4a10-8aa2-c89adf48a98d",
"5e9917bd-f3ac-41e6-8226-3fd79f340dc5",
"b9407f30-f5f8-466e-aff9-25556b57fe6d"]
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.5"
// 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
func startService(pubID: String) {
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 : 15
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
let prefs = UserDefaults.standard
var batch = prefs.array(forKey: AM_BATCH) ?? []
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"
batch.append(beaconJSON)
if batch.count > 600 {
batch.removeFirst(batch.count - 600)
}
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 > 3600 {
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>) {
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"] = "[]"
}
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let postString = getPostString(params: items)
request.httpBody = postString.data(using: .utf8)
let task = defaultSession.dataTask(with: request) { data, response, error in
self.clearBeaconBatch()
if (error == nil) {
self.markBeaconBatchSent()
}
}
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:

Swift
Objective-C
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
AreaMetrics.sharedInstance.startService(pubID: "PUBLISHER_ID")
return true
}

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: