diff --git a/.gitignore b/.gitignore index d5340449..a4b4a496 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output +.DS_Store diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/DemoApp/AppDelegate.swift b/DemoApp/AppDelegate.swift index 0706eb65..efe8f518 100644 --- a/DemoApp/AppDelegate.swift +++ b/DemoApp/AppDelegate.swift @@ -16,18 +16,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - - - let options = GeocoderRequest.GoogleOptions(APIKey: "AIzaSyBFNt-SA_YWs6avChK-sU5aMR3o7DRTH-8") - let google = GeocoderRequest.Service.google(options) - let x = LocationManager.shared.locateFromAddress("Via dei durantini 221, Rome", service: google) { data in - switch data { - case .failure(let error): - break - case .success(let value): - print(value) - } - } return true } diff --git a/DemoApp/Base.lproj/Main.storyboard b/DemoApp/Base.lproj/Main.storyboard index d8fd35f3..eae3d0b1 100644 --- a/DemoApp/Base.lproj/Main.storyboard +++ b/DemoApp/Base.lproj/Main.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -15,7 +13,7 @@ - + @@ -29,20 +27,20 @@ - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -721,13 +777,33 @@ + + + + + @@ -738,23 +814,26 @@ - + + + + @@ -784,6 +863,7 @@ + @@ -796,14 +876,14 @@ - + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - - + - - + - - + - + - + - + - - + - - + @@ -1311,7 +1551,7 @@ See the relative options class for more infos. - + diff --git a/DemoApp/Info.plist b/DemoApp/Info.plist index 2cdc4ec1..80b000bc 100644 --- a/DemoApp/Info.plist +++ b/DemoApp/Info.plist @@ -29,6 +29,8 @@ We are requesting user auth to read GPS - ALWAYS NSLocationWhenInUseUsageDescription We are requesting user auth to read GPS - ONLY IN USE + UIUserInterfaceStyle + Light UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/DemoApp/Monitor Controller/BeaconsRequestCell.swift b/DemoApp/Monitor Controller/BeaconsRequestCell.swift new file mode 100644 index 00000000..e0fd51c0 --- /dev/null +++ b/DemoApp/Monitor Controller/BeaconsRequestCell.swift @@ -0,0 +1,50 @@ +// +// GPSRequestCell.swift +// DemoApp +// +// Created by dan on 23/04/2019. +// Copyright © 2019 SwiftLocation. All rights reserved. +// + +import Foundation +import CoreLocation +import MapKit + +public class BeaconsRequestCell: UITableViewCell { + public static let height: CGFloat = 70 + + @IBOutlet public var titleLabel: UILabel! + @IBOutlet public var descriptionLabel: UILabel! + @IBOutlet public var stopButton: UIButton! + + internal weak var monitorController: RequestsMonitorController? + + @IBAction public func didPressStop() { + if let request = request { + switch request.state { + case .expired: + monitorController?.completedRequests.removeAll(where: { $0.id == request.id }) + monitorController?.reload() + default: + request.stop() + } + } + } + + public var request: BeaconsRequest? { + didSet { + stopButton.setTitle( (request?.state == .running ? "Stop" : "Remove"), for: .normal) + titleLabel.text = "BEACONS MONITORING" + + guard let beacons = request?.value else { + descriptionLabel.text = "(not received)" + return + } + + let descritpion = beacons.compactMap { "\($0.major) \($0.minor)" }.joined(separator: " | ") + descriptionLabel.text = descritpion + stopButton.isEnabled = true + stopButton.alpha = 1.0 + } + } +} diff --git a/DemoApp/Monitor Controller/RequestsMonitorController.swift b/DemoApp/Monitor Controller/RequestsMonitorController.swift index 63e18665..e4be97b0 100644 --- a/DemoApp/Monitor Controller/RequestsMonitorController.swift +++ b/DemoApp/Monitor Controller/RequestsMonitorController.swift @@ -19,11 +19,12 @@ class RequestsMonitorController: UIViewController { @IBOutlet public var currentAuth: UILabel! @IBOutlet public var currentAccuracy: UILabel! @IBOutlet public var countLocationReqs: UILabel! + @IBOutlet public var countBeaconsReqs: UILabel! @IBOutlet public var countLocationByIPReqs: UILabel! @IBOutlet public var countGeocodingReqs: UILabel! @IBOutlet public var countAutocompleteReqs: UILabel! @IBOutlet public var countHeadingReqs: UILabel! - + internal var completedRequests = [ServiceRequest]() private var timer: Timer? @@ -65,6 +66,10 @@ class RequestsMonitorController: UIViewController { self.present(NewGPSRequestController.create(), animated: true, completion: nil) })) + alert.addAction(UIAlertAction(title: "Monitor iBeacon", style: .default, handler: { _ in + self.present(NewBeaconsRequestController.create(), animated: true, completion: nil) + })) + alert.addAction(UIAlertAction(title: "Location by IP", style: .default, handler: { _ in self.present(NewIPRequestController.create(), animated: true, completion: nil) })) @@ -102,26 +107,28 @@ class RequestsMonitorController: UIViewController { extension RequestsMonitorController: UITableViewDataSource, UITableViewDelegate { func numberOfSections(in tableView: UITableView) -> Int { - return 6 // all kinds + completed requests + return 7 // all kinds + completed requests } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { let count = requestsForSection(section).count - if section != 5 && count == 0 { + if section != 6 && count == 0 { return nil } switch section { case 0: return "\(count) GPS" case 1: - return "\(count) IP LOCATION" + return "\(count) Beacons" case 2: - return "\(count) GEOCODING" + return "\(count) IP LOCATION" case 3: - return "\(count) HEADING" + return "\(count) GEOCODING" case 4: - return "\(count) AUTOCOMPLETE" + return "\(count) HEADING" case 5: + return "\(count) AUTOCOMPLETE" + case 6: return "COMPLETED REQUESTS" default: return nil @@ -133,14 +140,16 @@ extension RequestsMonitorController: UITableViewDataSource, UITableViewDelegate case 0: return Array(locator.queueLocationRequests) case 1: - return Array(locator.queueLocationByIPRequests) + return Array(locator.queueBeaconsRequests) case 2: - return Array(locator.queueGeocoderRequests) + return Array(locator.queueLocationByIPRequests) case 3: - return Array(locator.queueHeadingRequests) + return Array(locator.queueGeocoderRequests) case 4: - return Array(locator.queueAutocompleteRequests) + return Array(locator.queueHeadingRequests) case 5: + return Array(locator.queueAutocompleteRequests) + case 6: return completedRequests default: return [] @@ -168,6 +177,12 @@ extension RequestsMonitorController: UITableViewDataSource, UITableViewDelegate cell.request = locRequest cell.monitorController = self return cell + + case let beaconsRequest as BeaconsRequest: + let cell = tableView.dequeueReusableCell(withIdentifier: "BeaconsRequestCell") as! BeaconsRequestCell + cell.request = beaconsRequest + cell.monitorController = self + return cell case let ipRequest as LocationByIPRequest: let cell = tableView.dequeueReusableCell(withIdentifier: "IPRequestCell") as! IPRequestCell @@ -197,7 +212,10 @@ extension RequestsMonitorController: UITableViewDataSource, UITableViewDelegate switch request { case _ as LocationRequest: return GPSRequestCell.height - + + case _ as BeaconsRequest: + return BeaconsRequestCell.height + case _ as LocationByIPRequest: return IPRequestCell.height @@ -217,6 +235,7 @@ extension RequestsMonitorController: UITableViewDataSource, UITableViewDelegate countHeadingReqs.text = String(locator.queueHeadingRequests.count) countLocationReqs.text = String(locator.queueLocationRequests.count) + countBeaconsReqs.text = String(locator.queueBeaconsRequests.count) countGeocodingReqs.text = String(locator.queueGeocoderRequests.count) countAutocompleteReqs.text = String(locator.queueAutocompleteRequests.count) countLocationByIPReqs.text = String(locator.queueLocationByIPRequests.count) diff --git a/DemoApp/New Requests/NewBeaconsRequestController.swift b/DemoApp/New Requests/NewBeaconsRequestController.swift new file mode 100644 index 00000000..065816eb --- /dev/null +++ b/DemoApp/New Requests/NewBeaconsRequestController.swift @@ -0,0 +1,120 @@ +// +// NewGPSRequestController.swift +// SwiftLocation +// +// Created by dan on 23/04/2019. +// Copyright © 2019 SwiftLocation. All rights reserved. +// + +import UIKit +import CoreLocation + +public class NewBeaconsRequestController: UIViewController { + + @IBOutlet public var timeoutButton: UIButton! + @IBOutlet public var modeButton: UIButton! + @IBOutlet public var distanceFilter: UITextField! + @IBOutlet public var proximityUUID: UITextField! + @IBOutlet public var activityButton: UIButton! + + private var timeout: Timeout.Mode? = nil { + didSet { + reload() + } + } + + private var mode: BeaconsRequest.Subscription = .oneShot { + didSet { + reload() + } + } + + private var activityType: CLActivityType = .other { + didSet { + reload() + } + } + + public static func create() -> UINavigationController { + let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main) + let vc = storyboard.instantiateViewController(withIdentifier: "NewBeaconsRequestController") as! NewBeaconsRequestController + return UINavigationController(rootViewController: vc) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didPressCancel)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Create", style: .plain, target: self, action: #selector(createRequest)) + + self.timeout = .delayed(10) + self.mode = .oneShot + reload() + } + + @objc func didPressCancel() { + self.dismiss(animated: true, completion: nil) + } + + + @IBAction public func setMode() { + let options: [SelectionItem] = BeaconsRequest.Subscription.all.map { + return SelectionItem(title: $0.description, value: $0) + } + self.showPicker(title: "Select a Subscription mode", msg: nil, options: options, onSelect: { item in + self.mode = item.value! + }) + } + + @IBAction public func setActivityType() { + var options: [SelectionItem] = [ + SelectionItem(title: "other", value: CLActivityType.other), + SelectionItem(title: "automotiveNavigation", value: CLActivityType.automotiveNavigation), + SelectionItem(title: "fitness", value: CLActivityType.fitness), + SelectionItem(title: "otherNavigation", value: CLActivityType.otherNavigation), + ] + + if #available(iOS 12.0, *) { + options.append(SelectionItem(title: "airborne", value: CLActivityType.airborne)) + } + + self.showPicker(title: "Select an Activity", msg: nil, options: options, onSelect: { item in + self.activityType = item.value! + }) + } + + @IBAction public func setTimeout() { + let options: [SelectionItem] = [ + .init(title: "Absolute 5s", value: .absolute(5)), + .init(title: "Absolute 10s", value: .absolute(10)), + .init(title: "Absolute 20s", value: .absolute(20)), + .init(title: "Delayed 5s", value: .delayed(5)), + .init(title: "Delayed 10s", value: .delayed(10)), + .init(title: "Delayed 20s", value: .delayed(20)), + .init(title: "No Timeout", value: nil), + ] + self.showPicker(title: "Select a Timeout", msg: nil, options: options, onSelect: { item in + self.timeout = item.value + }) + } + + private func reload() { + timeoutButton.setTitle(timeout?.description ?? "not set", for: .normal) + modeButton.setTitle(mode.description, for: .normal) + activityButton.setTitle(activityType.description, for: .normal) + } + + @objc public func createRequest() { + guard let proximityUUID = UUID(uuidString: proximityUUID.text ?? "") else { + let alert = UIAlertController(title: "Invalid Proximity UUID", message: "Invalid identifier of the beacon.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) + self.present(alert, animated: true, completion: nil) + return + } + + LocationManager.shared.locateFromBeacons(self.mode, + proximityUUID: proximityUUID, + result: nil) + self.dismiss(animated: true, completion: nil) + } +} diff --git a/DemoApp/New Requests/NewGPSRequestController.swift b/DemoApp/New Requests/NewGPSRequestController.swift index 33f262d1..fcce0a37 100644 --- a/DemoApp/New Requests/NewGPSRequestController.swift +++ b/DemoApp/New Requests/NewGPSRequestController.swift @@ -126,8 +126,9 @@ public class NewGPSRequestController: UIViewController { accuracy: self.accuracy, distance: CLLocationDistance(distanceFilter.text ?? "-1"), activity: self.activityType, - timeout: self.timeout, - result: nil) + timeout: self.timeout) { result in + print("\(result)") + } self.dismiss(animated: true, completion: nil) } } diff --git a/Package.swift b/Package.swift index 4e357c62..20dad1ee 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,9 @@ import PackageDescription let package = Package( name: "SwiftLocation", + platforms: [ + .iOS("9.3") + ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( diff --git a/README.md b/README.md index c07ea81e..cb9b9d0c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

-SwiftDate +SwiftLocation

-### Efficient and easy to use location tracking framework for iOS +### Efficient and easy to use location tracking, geocoding, autocomplete & beacon framework for iOS | | Main Features | |---- |---------------------------------------------------------------------------- | @@ -13,6 +13,7 @@ | 🌏 | Support for Geocoding/Reverse Geocoding (Apple, Google, OpenStreet) | | 🔍 | Support for Autocomplete/Place Details (Apple, Google) | | 🖥 | Support IP based location with multiple pluggable services | +| 📍 | Support iBeacon tracking | | ⏱ | Support continous location monitoring with fixed minumum time interval / min distance | @@ -74,6 +75,7 @@ Using this lightweight library you will not need to struggle with CoreLocation's - [Heading Updates](#heading_updates) - [Geocoding/Reverse Geocoding](#geocoding) - [Autocomplete](#autocomplete) +- [iBeacon Tracking](#ibeacon) @@ -443,6 +445,63 @@ LocationManager.shared.autocomplete(partialMatch: .partialSearch("Piazza della R } ``` + + +## iBeacon Tracking + +Since 4.2.0 SwiftLocation also support iBeacon's beacons tracking. + +An iBeacon is a device that emits a Bluetooth signal that can be detected by your devices. Companies can deploy iBeacon devices in environments where proximity detection is a benefit to users, and apps can use the proximity of beacons to determine an appropriate course of action. You decide what actions to take based on the proximity of nearby beacons. For example, a department store might deploy beacons identifying each section of the store, and the corresponding app might point out sale items when the user is near each section. + +When deploying your iBeacon hardware, you must program each iBeacon with an appropriate proximity UUID, major value, and minor value. These values identify each of your beacons uniquely and make it possible for your app to differentiate between those beacons later. + +- The uuid (universally unique identifier) is a 128-bit value that uniquely identifies your app’s beacons. +- The major value is a 16-bit unsigned integer that you use to differentiate groups of beacons with the same UUID. +- The minor value is a 16-bit unsigned integer that you use to differentiate groups of beacons with the same UUID and major value. + +Only the UUID is required, but it is recommended that you program all three values into your iBeacon hardware. In your app, you can look for related groups of beacons by specifying only a subset of values. + +Tracking a beacon with SwiftLocation is very simple. + +```swift +// The UUID is a 128-bit value that uniquely identifies your app’s beacons. +let proximityUUID = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! +LocationManager.shared.locateFromBeacons(.continous, proximityUUID: proximityUUID, result: { result in + switch result { + case .failure(let error): + // something went wrong + case .success(let beaconsFound): + // beacons found + doSomethingWithBeacons(beaconsFound) + } +}) + +func doSomethingWithBeacons(_ beacons: [CLBeacon]) { + guard beacons.isEmpty == false else { + return + } + + let nearestBeacon = beacons.first! + let major = CLBeaconMajorValue(nearestBeacon.major) + let minor = CLBeaconMinorValue(nearestBeacon.minor) + + switch nearestBeacon.proximity { + case .near, .immediate: + // Display information about the relevant exhibit. + break + + default: + // Dismiss exhibit information, if it is displayed. + break + } + } +} +``` + +### Tip + +When deploying beacons, consider giving each one a unique combination of UUID, major, and minor values so that you can distinguish among them. If multiple beacons use the same UUID, major, and minor values, the array of beacons delivered to the request reponse method might be differentiated only by their proximity and accuracy values. + ## Copyright SwiftLocation is currently owned and maintained by Daniele Margutti. diff --git a/Sources/SwiftLocation/Auto Complete/AutoComplete+Support.swift b/Sources/SwiftLocation/Auto Complete/AutoComplete+Support.swift index d2701ec7..c23a8ae6 100644 --- a/Sources/SwiftLocation/Auto Complete/AutoComplete+Support.swift +++ b/Sources/SwiftLocation/Auto Complete/AutoComplete+Support.swift @@ -96,6 +96,8 @@ public extension AutoCompleteRequest { return [] } + public init() { } + } class GoogleOptions: Options { diff --git a/Sources/SwiftLocation/Beacons/BeaconsRequest.swift b/Sources/SwiftLocation/Beacons/BeaconsRequest.swift new file mode 100644 index 00000000..57c91da8 --- /dev/null +++ b/Sources/SwiftLocation/Beacons/BeaconsRequest.swift @@ -0,0 +1,208 @@ +// +// SwiftLocation - Efficient Location Tracking for iOS +// +// Created by Daniele Margutti +// - Web: https://www.danielemargutti.com +// - Twitter: https://twitter.com/danielemargutti +// - Mail: hello@danielemargutti.com +// +// Copyright © 2019 Daniele Margutti. Licensed under MIT License. + +import Foundation +import CoreLocation + +/// BeaconsRequest represent the request entity which contains a reference +/// to subscriber, a list of contraints to evaluate. +/// A reference is keep on queue until its valid and you can manage the subscription +// directly from here. +public class BeaconsRequest: ServiceRequest, Hashable { + + // MARK: - Typealiases - + + public typealias Data = Result<[CLBeacon], LocationManager.ErrorReason> + public typealias Callback = ((Data) -> Void) + + // MARK: - Private Properties - + + /// Timeout manager handles timeout events. + internal var timeoutManager: Timeout? { + didSet { + // Also set the callback to receive timeout event; it will remove the request. + timeoutManager?.callback = { interval in + self.stop(reason: .requiredBeaconsNotFound(timeout: interval, last: self.lastAbsoluteBeacons), remove: true) + } + } + } + + // MARK: - Public Properties - + + /// Last obtained valid value for request. + public internal(set) var value: [CLBeacon]? + + /// Type of timeout set. + public var timeout: Timeout.Mode? { + return timeoutManager?.mode + } + + /// Unique identifier of the request. + public var id: ServiceRequest.RequestID + + + /// Proximity identifier of iBeacon to monitor + public var proximityUUID: UUID + + /// Callbacks called once a new iBeacon or error is received. + public var observers = Observers() + + /// Last received iBeacons (even if not valid). + public private(set) var lastAbsoluteBeacons: [CLBeacon]? + + /// Last valid received iBeacons (only if meet request's criteria). + public private(set) var lastBeacons: [CLBeacon]? + + /// Current state of the request. + public internal(set) var state: RequestState = .idle + + /// Subscription mode used to receive events. + public internal(set) var subscription: Subscription = .oneShot + + /// You can provide a custom validation rule which overrides the default settings for + /// accuracy and time threshold. You will receive in this callback any location retrived + /// from the GPS system and you can decide if it's valid to be propagated or not. + /// Inside the callback you will receive the location and the time interval between now + /// and the time you have received the location. + public var customValidation: (([CLBeacon]) -> Bool)? = nil + + // MARK: - Initialization - + + internal init(proximityUUID: UUID) { + self.id = UUID().uuidString + self.proximityUUID = proximityUUID + } + + // MARK: - Public Methods - + + /// Stop the request and remove it from queue. + /// Request will be marked as `expired`. + public func stop() { + stop(reason: .cancelled, remove: true) + } + + /// Complete a request with given CLBeacons. + /// If subscription mode is continous the event will be passed to callbacks and + /// request still alive receiving other events; in case of one shot request + /// it fulfill the request itself and remove it from queue. + /// + /// - Parameter beacons: CLBeacon to pass. + internal func complete(beacons: [CLBeacon]) { + lastBeacons = beacons + guard state.canReceiveEvents && beaconsSatisfyRequest(beacons) else { + return // ignore events + } + + // We can stop the timeout timer, the first valid event has been received. + timeoutManager?.reset() + value = beacons + dispatch(data: .success(beacons)) // dispatch to callbacks + if subscription == .oneShot { // one shot events will be removed + LocationManager.shared.removeBeaconTracking(self) + } + } + + /// Mark the request as paused. It still remain in queue but any received event (valid or not) + /// will be discarded and not passed to the subscribed callbacks. + public func pause() { + state = .paused + } + + /// Start/restart a [paused/idle/expired] request. + public func start() { + LocationManager.shared.startBeaconTracking(self) // add to queue + } + + // MARK: - Protocols Conformances - + + public static func == (lhs: BeaconsRequest, rhs: BeaconsRequest) -> Bool { + return lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + // MARK: - Internal Methods - + + /// Stop a request with passed error reason and optionally remove it from queue. + /// + /// - Parameters: + /// - reason: reason of failure. + /// - remove: `true` to also remove it from queue. + internal func stop(reason: LocationManager.ErrorReason = .cancelled, remove: Bool) { + defer { + if remove { + LocationManager.shared.removeBeaconTracking(self) + } + } + timeoutManager?.reset() + dispatch(data: .failure(reason)) + } + + /// Dispatch received events to all callbacks. + /// + /// - Parameter data: data to pass. + internal func dispatch(data: Data) { + observers.list.forEach { + $0(data) + } + } + + /// Return `true` if received CLBeacon satisfy the constraint of the request and can be dispatched to its observers. + /// + /// - Parameter beacons: CLBeacons to evaluate. + /// - Returns: `true` if valid, `false` otherwise. + private func beaconsSatisfyRequest(_ beacons: [CLBeacon]) -> Bool { + if let customValidationRule = customValidation { + // overridden by custom validator rule + return customValidationRule(beacons) + } + return true + } + + /// Restart a request which are not paused. + internal func switchToRunningIfNotPaused() { + switch state { + case .paused: + break + default: + state = .running + } + } + +} + +// MARK: - Subscription - + +public extension BeaconsRequest { + + /// Type of subscription for events. + /// + /// - oneShot: one shot subscription. Once fulfilled or rejected request will be removed automatically. + /// - continous: continous subscription still produces events (error or valid locations) until it will be removed manually. + /// - significant: significant subsription sitll produces events (error or valid significant location) until removed. + enum Subscription: CustomStringConvertible { + case oneShot + case continous + + public static var all: [Subscription] { + return [.oneShot, .continous] + } + + public var description: String { + switch self { + case .oneShot: return "oneShot" + case .continous: return "continous" + } + } + } + +} diff --git a/Sources/SwiftLocation/LocationManager+Support.swift b/Sources/SwiftLocation/LocationManager+Support.swift index 2a24cfc3..96835049 100644 --- a/Sources/SwiftLocation/LocationManager+Support.swift +++ b/Sources/SwiftLocation/LocationManager+Support.swift @@ -32,8 +32,19 @@ public extension LocationManager { case noData(URL?) case missingAPIKey case requiredLocationNotFound(timeout: TimeInterval, last: CLLocation?) + case requiredBeaconsNotFound(timeout: TimeInterval, last: [CLBeacon]?) + + static func errorReason(from error: Error) -> ErrorReason { + if let locationError = error as? CLError { + if locationError == CLError(.denied) || locationError == CLError(.regionMonitoringDenied) { + return .invalidAuthStatus(.denied) + } + } + return .generic(error.localizedDescription) + } } + enum State: CustomStringConvertible { case available case undetermined @@ -104,9 +115,9 @@ public extension LocationManager { case .block: return 100 case .house: - return 15 + return 60 case .room: - return 5 + return 25 case .custom(let value): return value } @@ -121,9 +132,9 @@ public extension LocationManager { case .block: return 60.0 case .house: - return 15.0 + return 40.0 case .room: - return 5.0 + return 20.0 default: return TimeInterval.greatestFiniteMagnitude } diff --git a/Sources/SwiftLocation/LocationManager.swift b/Sources/SwiftLocation/LocationManager.swift index 98907c2a..96a67062 100644 --- a/Sources/SwiftLocation/LocationManager.swift +++ b/Sources/SwiftLocation/LocationManager.swift @@ -18,6 +18,7 @@ public class LocationManager: NSObject { public typealias RequestID = String internal typealias LocationRequestSet = Set + internal typealias BeaconsRequestSet = Set internal typealias GeocoderRequestSet = Set internal typealias AutocompleteRequestSet = Set internal typealias IPRequestSet = Set @@ -120,7 +121,11 @@ public class LocationManager: NSObject { /// This is the list of the requests currently in queue. /// List is thread safe in read/write. internal private(set) var queueLocationRequests: LocationRequestSet - + + /// This is the list of the requests currently in queue. + /// List is thread safe in read/write. + internal private(set) var queueBeaconsRequests: BeaconsRequestSet + /// This is the list of all geocoder (reverse/not reverse) requests currently active. internal private(set) var queueGeocoderRequests: GeocoderRequestSet @@ -153,6 +158,7 @@ public class LocationManager: NSObject { internal override init() { queueLocationRequests = LocationRequestSet() + queueBeaconsRequests = BeaconsRequestSet() queueGeocoderRequests = GeocoderRequestSet() queueAutocompleteRequests = AutocompleteRequestSet() queueLocationByIPRequests = IPRequestSet() @@ -210,6 +216,32 @@ public class LocationManager: NSObject { let _ = request.start() return request } + + /// Create and enque a request to get the current device's location. + /// + /// - Parameters: + /// - subscription: type of subscription you want to set. + /// - distance: The minimum distance (measured in meters) a device must move horizontally before an update event is generated. + /// - activity: The location manager uses the information in this property as a cue to determine when location updates + /// may be automatically paused. + /// - timeout: if set a valid timeout interval to set; if you don't receive events in this interval requests will expire. + /// - result: callback where you will receive the result of request. + /// - Returns: return the request itself you can use to manage the lifecycle. + @discardableResult + public func locateFromBeacons(_ subscription: BeaconsRequest.Subscription, + proximityUUID: UUID, + timeout: Timeout.Mode? = .delayed(LocationManager.shared.timeout), + result: BeaconsRequest.Callback?) -> BeaconsRequest { + let request = BeaconsRequest(proximityUUID: proximityUUID) + // only one shot requests has timeout + request.timeoutManager = (subscription == .oneShot ? (timeout != nil ? Timeout(mode: timeout!) : nil) : nil) + request.subscription = subscription + if let result = result { + request.observers.add(result) + } + let _ = request.start() + return request + } /// Return device's approximate location by using one of the specified services. /// Some services may require subscription and return approximate locations without requiring explicit permission to the user. @@ -496,6 +528,45 @@ public class LocationManager: NSObject { dispatchQueueChangeEvent(true, request: request) } + // Use last known location for added location if location manager is active + if queueLocationRequests.count(where: { $0.state == .running && $0.subscription == .continous }) > 0, + let location = manager.location { + request.complete(location: location) + } + + updateLocationManagerSettings(request) + return true + } + + // MARK: - Private Methods: Beacons Location - + + /// Remove location from the list of requests. + /// + /// - Parameter request: request to remove. + internal func removeBeaconTracking(_ request: BeaconsRequest) { + request.state = .expired + if let _ = queueBeaconsRequests.remove(request) { + dispatchQueueChangeEvent(false, request: request) + updateLocationManagerSettings(request) + } + } + + /// Start a new request. + /// + /// - Parameter request: request to start. + /// - Returns: `true` if added correctly to the queue, `false` otherwise. + @discardableResult + internal func startBeaconTracking(_ request: BeaconsRequest) -> Bool { + guard request.state.isRunning == false else { + return true + } + request.state = (LocationManager.state == .available ? .running : .idle) // change the state + request.timeoutManager?.startIfNeeded() + let result = queueBeaconsRequests.insert(request) + if result.inserted { + dispatchQueueChangeEvent(true, request: request) + } + updateLocationManagerSettings(request) return true } @@ -536,7 +607,7 @@ public class LocationManager: NSObject { case .oneShot, .continous: // Request authorization only if needed manager.requestAuthorizationIfNeeded(preferredAuthorization) - guard countRequestsInStates([.idle,.running]) > 0 || LocationManager.state != .available else { + guard queueLocationRequests.count(where: { [.idle, .running].contains($0.state) }) > 0 || LocationManager.state != .available else { // if no running requests are active we can stop monitoring manager.stopUpdatingLocation() return @@ -557,12 +628,28 @@ public class LocationManager: NSObject { manager.startMonitoringSignificantLocationChanges() break } + } + + /// Adjust the location manager settings based upon the currently running requests and new added request. + /// + /// - Parameter request: request added to queue. + private func updateLocationManagerSettings(_ request: BeaconsRequest) { + // Request authorization always for beacons access + manager.requestAuthorizationIfNeeded(preferredAuthorization) + + let beaconRegion = CLBeaconRegion(proximityUUID: request.proximityUUID, identifier: request.id) + guard queueBeaconsRequests.count(where: { [.idle,.running].contains($0.state) }) > 0 else { + // if no running requests are active we can stop monitoring + manager.stopMonitoring(for: beaconRegion) + return + } + manager.startMonitoring(for: beaconRegion) } internal func dispatchQueueChangeEvent(_ new: Bool, request: ServiceRequest) { onQueueChange.list.forEach { - $0(new,request) + $0(new, request) } } @@ -574,16 +661,11 @@ public class LocationManager: NSObject { $0.stop(reason: error, remove: true) updateLocationManagerSettings($0) } - } - - /// Count the number of requests enqueued into the list of states. - /// - /// - Parameter states: states to filter. - /// - Returns: `Int` - private func countRequestsInStates(_ states: Set) -> Int { - return queueLocationRequests.count(where: { - states.contains($0.state) - }) + + queueBeaconsRequests.forEach { + $0.stop(reason: error, remove: true) + updateLocationManagerSettings($0) + } } } @@ -622,6 +704,12 @@ extension LocationManager: CLLocationManagerDelegate { $0.switchToRunningIfNotPaused() $0.timeoutManager?.startIfNeeded() } + + queueBeaconsRequests.forEach { + $0.switchToRunningIfNotPaused() + $0.timeoutManager?.startIfNeeded() + } + } } @@ -637,9 +725,36 @@ extension LocationManager: CLLocationManagerDelegate { public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { for request in queueLocationRequests { // dispatch the error to any request - let shouldRemove = !(request.subscription == .oneShot) // oneshot location will be removed in this case + let shouldRemove = request.subscription == .oneShot // oneshot location will be removed in this case + request.stop(reason: ErrorReason.errorReason(from: error), remove: shouldRemove) + } + + for request in queueBeaconsRequests { // dispatch the error to any request + let shouldRemove = request.subscription == .oneShot // oneshot location will be removed in this case + request.stop(reason: ErrorReason.errorReason(from: error), remove: shouldRemove) + } + + } + + // MARK: - CoreLocationManagerDelegate for iBeacons + + public func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) { + for request in queueBeaconsRequests.filter ({ $0.id == region?.identifier }) { // dispatch location to any request + let shouldRemove = request.subscription == .oneShot // oneshot location will be removed in this case request.stop(reason: .generic(error.localizedDescription), remove: shouldRemove) } } + public func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) { + let hasRequestsForRegion = queueBeaconsRequests.filter ({ $0.id == region.identifier }).count > 0 + if hasRequestsForRegion, let region = region as? CLBeaconRegion { + manager.startRangingBeacons(in: region) + } + } + + public func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) { + for request in queueBeaconsRequests.filter ({ $0.id == region.identifier }) { // dispatch location to any request + request.complete(beacons: beacons) + } + } } diff --git a/SwiftLocation.podspec b/SwiftLocation.podspec index 3456e990..7bc3f5ac 100644 --- a/SwiftLocation.podspec +++ b/SwiftLocation.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SwiftLocation" - s.version = "4.1.0" + s.version = "4.2.0" s.summary = "Easy and Efficient Location Tracking for iOS" s.description = <<-DESC Efficient location tracking for iOS with support for oneshot/continuous/background tracking, reverse geocoding, autocomplete and more! diff --git a/SwiftLocation.xcodeproj/project.pbxproj b/SwiftLocation.xcodeproj/project.pbxproj index 85c3c2a4..4ee374d2 100644 --- a/SwiftLocation.xcodeproj/project.pbxproj +++ b/SwiftLocation.xcodeproj/project.pbxproj @@ -76,6 +76,10 @@ 64A802BC2269FD8500FDCB2F /* IPAPICoRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A802B72269FD8500FDCB2F /* IPAPICoRequest.swift */; }; 8933C7851EB5B820000D00A4 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7841EB5B820000D00A4 /* LocationManager.swift */; }; 8933C7901EB5B82D000D00A4 /* SwiftLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7891EB5B82A000D00A4 /* SwiftLocationTests.swift */; }; + B818C1112321786F0002CA31 /* BeaconsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B818C1102321786F0002CA31 /* BeaconsRequest.swift */; }; + B818C112232182580002CA31 /* BeaconsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B818C1102321786F0002CA31 /* BeaconsRequest.swift */; }; + B818C114232182C50002CA31 /* NewBeaconsRequestController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B818C113232182C50002CA31 /* NewBeaconsRequestController.swift */; }; + B818C1162321898D0002CA31 /* BeaconsRequestCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B818C1152321898D0002CA31 /* BeaconsRequestCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -146,6 +150,9 @@ 8933C7891EB5B82A000D00A4 /* SwiftLocationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftLocationTests.swift; sourceTree = ""; }; AD2FAA261CD0B6D800659CF4 /* SwiftLocation.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = SwiftLocation.plist; sourceTree = ""; }; AD2FAA281CD0B6E100659CF4 /* SwiftLocationTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = SwiftLocationTests.plist; sourceTree = ""; }; + B818C1102321786F0002CA31 /* BeaconsRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeaconsRequest.swift; sourceTree = ""; }; + B818C113232182C50002CA31 /* NewBeaconsRequestController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NewBeaconsRequestController.swift; path = "DemoApp/New Requests/NewBeaconsRequestController.swift"; sourceTree = SOURCE_ROOT; }; + B818C1152321898D0002CA31 /* BeaconsRequestCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeaconsRequestCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -279,6 +286,7 @@ children = ( 646A5585226376F0001CD502 /* RequestsMonitorController.swift */, 647D19B9226F16930034FBD7 /* GPSRequestCell.swift */, + B818C1152321898D0002CA31 /* BeaconsRequestCell.swift */, 647D19BD226F1ACF0034FBD7 /* IPRequestCell.swift */, 6427B50D226F28F600DCCC24 /* GeocodeRequestCell.swift */, 6427B511226F42CA00DCCC24 /* AutocompleteRequestCell.swift */, @@ -290,6 +298,7 @@ isa = PBXGroup; children = ( 647D19B0226EF4310034FBD7 /* NewGPSRequestController.swift */, + B818C113232182C50002CA31 /* NewBeaconsRequestController.swift */, 6427B509226F1D5200DCCC24 /* NewIPRequestController.swift */, 6427B50B226F20FA00DCCC24 /* NewGeocodingRequestController.swift */, 6427B50F226F328600DCCC24 /* NewAutocompleteRequestController.swift */, @@ -371,6 +380,7 @@ 6492FFA2226868DE00414FE5 /* LocationManager+Support.swift */, 646A55A322638A3F001CD502 /* ServiceRequest.swift */, 646A55B52263C818001CD502 /* Locating */, + B818C10F2321786F0002CA31 /* Beacons */, 64A802A92269DCD900FDCB2F /* Locating By IP */, 647D19A3226B42CC0034FBD7 /* Heading */, 64A675F52266EF1B00A1C011 /* Geocoding */, @@ -391,6 +401,14 @@ path = Tests/SwiftLocationTests; sourceTree = ""; }; + B818C10F2321786F0002CA31 /* Beacons */ = { + isa = PBXGroup; + children = ( + B818C1102321786F0002CA31 /* BeaconsRequest.swift */, + ); + path = Beacons; + sourceTree = ""; + }; DD7502721C68FC1B006590AF /* Frameworks */ = { isa = PBXGroup; children = ( @@ -512,7 +530,7 @@ }; 646A5580226376F0001CD502 = { CreatedOnToolsVersion = 10.2; - DevelopmentTeam = GHLS8P2M5V; + DevelopmentTeam = E5DU3FA699; ProvisioningStyle = Automatic; }; 646A5593226376F1001CD502 = { @@ -592,6 +610,7 @@ 64A802AB2269DD2F00FDCB2F /* LocationByIPRequest+Support.swift in Sources */, 6492FFB1226869D800414FE5 /* PlaceMatch.swift in Sources */, 6492FFBD22686B2E00414FE5 /* AppleAutoCompleteRequest.swift in Sources */, + B818C1112321786F0002CA31 /* BeaconsRequest.swift in Sources */, 64A802B82269FD8500FDCB2F /* IPAPICoRequest.swift in Sources */, 64712C8A2267B69E00B320DA /* OpenStreetGeocoderRequest.swift in Sources */, 647D199E226B42CA0034FBD7 /* HeadingRequest.swift in Sources */, @@ -648,11 +667,14 @@ 646A55A2226376F9001CD502 /* Support.swift in Sources */, 6492FFB5226869D800414FE5 /* PlaceMatch.swift in Sources */, 647D19B7226EF9FE0034FBD7 /* Extensions.swift in Sources */, + B818C114232182C50002CA31 /* NewBeaconsRequestController.swift in Sources */, 641AB6F42264704F00778DC8 /* AppleGeocoderRequest.swift in Sources */, + B818C112232182580002CA31 /* BeaconsRequest.swift in Sources */, 646A55AB22639780001CD502 /* Timeout.swift in Sources */, 647D19A2226B42CA0034FBD7 /* HeadingRequest.swift in Sources */, 6492FFA7226868DE00414FE5 /* LocationManager+Support.swift in Sources */, 64A6762C22670C9400A1C011 /* JSONOperation.swift in Sources */, + B818C1162321898D0002CA31 /* BeaconsRequestCell.swift in Sources */, 646A5584226376F0001CD502 /* AppDelegate.swift in Sources */, 6427B512226F42CA00DCCC24 /* AutocompleteRequestCell.swift in Sources */, 647D19B5226EF4310034FBD7 /* NewGPSRequestController.swift in Sources */, @@ -905,7 +927,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = GHLS8P2M5V; + DEVELOPMENT_TEAM = E5DU3FA699; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = DemoApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -932,7 +954,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = GHLS8P2M5V; + DEVELOPMENT_TEAM = E5DU3FA699; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = DemoApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0;