iOS Beacon Detection Snippet
Guide to getting started with the AreaMetrics beacon monitoring platform.
Add the AreaMetrics.swift file to your project
//
// 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
Ask the user for Location Permission
iOS Location PermissionsLast updated