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
}

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 Permissions

Last updated