import UIKit import CoreLocation import CocoaLumberjackSwift public struct LocationManagerConfiguration { let accuracy: CLLocationAccuracy let filter: CLLocationDistance var activityType: CLActivityType = .fitness } extension LocationManagerConfiguration { /// A configuration preset for fine location updates. public static let fine = LocationManagerConfiguration( accuracy: kCLLocationAccuracyBest, filter: 1 ) /// A configuration preset for coarse location updates. public static let coarse = LocationManagerConfiguration( accuracy: kCLLocationAccuracyKilometer, filter: 1000 ) } // MARK: - LocationManagerDelegate @objc public protocol LocationManagerDelegate: AnyObject { @objc(locationManager:didUpdateLocation:) optional func locationManager(_ locationManager: LocationManagerProtocol, didUpdate location: CLLocation) @objc(locationManager:didUpdateHeading:) optional func locationManager(_ locationManager: LocationManagerProtocol, didUpdate heading: CLHeading) @objc(locationManager:didReceiveError:) optional func locationManager(_ locationManager: LocationManagerProtocol, didReceive error: Error) @objc(locationManager:didUpdateAuthorizedState:) optional func locationManager(_ locationManager: LocationManagerProtocol, didUpdateAuthorized authorized: Bool) } final public class LocationManager: NSObject { /// The latest known location. public private(set) lazy var location: CLLocation? = self.locationManager.location /// The latest known heading. public private(set) lazy var heading: CLHeading? = self.locationManager.heading /// Returns `true` when the location and heading monitoring is active. public private(set) var isUpdating = false /// Set the delegate if you want to receive location and heading updates. public weak var delegate: LocationManagerDelegate? /// Returns the current locationManager permission state. public var authorizationStatus: CLAuthorizationStatus { locationManager.authorizationStatus } /// Starts monitoring location and heading updates. public func startMonitoringLocation() { guard isUpdating == false else { return } guard authorizationStatus != .notDetermined else { authorize(success: startMonitoringLocation) DDLogVerbose("LocationManager - skip monitoring location because status is \(authorizationStatus.rawValue).") return } guard authorizationStatus.isAuthorized else { // Start monitoring location in case the authorization status ever changes to authorized. authorizedCompletion = startMonitoringLocation return } locationManager.startUpdatingLocation() startUpdatingHeading() isUpdating = true DDLogDebug("LocationManager - did start updating location & heading.") } /// Stops monitoring location and heading updates. public func stopMonitoringLocation() { locationManager.stopUpdatingLocation() stopUpdatingHeading() isUpdating = false DDLogDebug("LocationManager - did stop updating location & heading.") } /// Creates a new instance of `LocationManager`. /// /// - Parameters: /// - locationManager: If needed, provide a custom `CLLocationManager` instance. The default /// value creates a new instance. /// - device: If needed, provide a custom `UIDevice` instance. The default value is `UIDevice.current`. /// - configuration: The configuration used for configuring `CLLocationManager`. The default value /// is the `.fine` preset. /// public init( locationManager: CLLocationManager = .init(), device: UIDevice = .current, configuration: LocationManagerConfiguration = .fine ) { assert(Thread.isMainThread, "Location managers must be created on the main thread") locationManager.distanceFilter = configuration.filter locationManager.desiredAccuracy = configuration.accuracy locationManager.activityType = configuration.activityType self.locationManager = locationManager self.device = device super.init() locationManager.delegate = self } deinit { stopMonitoringLocation() locationManager.delegate = nil } // MARK: - Private private let locationManager: CLLocationManager private let device: UIDevice // MARK: - Authorization /// The completion closure invoked when `authorizationStatus` changes to `authorizedAlways` /// or `authorizedWhenInUse`. private var authorizedCompletion: (() -> Void)? private func authorize(success: (() -> Void)? = nil) { authorizedCompletion = success locationManager.requestWhenInUseAuthorization() DDLogInfo("LocationManager - requesting authorization to access location when in use.") } // MARK: - Heading /// The observer token for `UIDevice.orientationDidChangeNotification`. private var orientationObserver: NSObjectProtocol? private func startUpdatingHeading() { guard !isUpdating else { return } device.beginGeneratingDeviceOrientationNotifications() updateHeadingOrientation() locationManager.startUpdatingHeading() orientationObserver = NotificationCenter.default.addObserver( forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil ) { [weak self] _ in self?.updateHeadingOrientation() } } private func stopUpdatingHeading() { guard isUpdating else { return } device.endGeneratingDeviceOrientationNotifications() locationManager.stopUpdatingHeading() if let observer = orientationObserver { NotificationCenter.default.removeObserver(observer) orientationObserver = nil } } private func updateHeadingOrientation() { locationManager.headingOrientation = device.orientation.clOrientation } } // MARK: - LocationManagerDelegate extension LocationManager: CLLocationManagerDelegate { public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard isUpdating, let location = locations.last else { return } self.location = location delegate?.locationManager?(self, didUpdate: location) DDLogVerbose("LocationManager - did update location: \(location).") } public func locationManager(_ manager: CLLocationManager, didUpdateHeading heading: CLHeading) { guard isUpdating, heading.headingAccuracy > 0 else { return } self.heading = heading delegate?.locationManager?(self, didUpdate: heading) DDLogVerbose("LocationManager - did update heading: \(heading).") } public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { guard isUpdating else { DDLogVerbose("LocationManager - suppressing error received after call to stop monitoring location: \(error)") return } #if targetEnvironment(simulator) let nsError = error as NSError guard !(nsError.domain == kCLErrorDomain && nsError.code == CLError.locationUnknown.rawValue) else { return } #endif delegate?.locationManager?(self, didReceive: error) DDLogError("LocationManager - encountered error: \(error).") } public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = manager.authorizationStatus DDLogInfo("LocationManager - did change authorization status \(status.rawValue).") delegate?.locationManager?(self, didUpdateAuthorized: status.isAuthorized) if status.isAuthorized { authorizedCompletion?() authorizedCompletion = nil } } } // MARK: LocationManagerProtocol extension LocationManager: LocationManagerProtocol { public var isAuthorized: Bool { authorizationStatus.isAuthorized } } public extension CLAuthorizationStatus { var isAuthorized: Bool { self == .authorizedAlways || self == .authorizedWhenInUse } } private extension UIDeviceOrientation { var clOrientation: CLDeviceOrientation { switch self { case .portrait: return .portrait case .portraitUpsideDown: return .portraitUpsideDown case .landscapeLeft: return .landscapeLeft case .landscapeRight: return .landscapeRight case .faceUp: return .faceUp case .faceDown: return .faceDown default: return .unknown } } }