Last active
October 5, 2021 12:34
-
-
Save tadija/dcdf25983c0dbf410e430221f74b20df to your computer and use it in GitHub Desktop.
AELocation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* https://gist.github.com/tadija/dcdf25983c0dbf410e430221f74b20df | |
* Revision 4 | |
* Copyright © 2016-2021 Marko Tadić | |
* Licensed under the MIT license | |
*/ | |
import CoreLocation | |
@available(iOS 9.0, *) | |
public final class AELocation: NSObject { | |
public enum AuthorizationType { | |
case whenInUse | |
case always | |
} | |
public indirect enum Error: Swift.Error { | |
case locationServiceDisabled | |
case locationMissing | |
case reverseGeocodeFailed | |
case systemError(Swift.Error) | |
} | |
// MARK: Properties | |
public var mocked: CLLocation? { | |
didSet { | |
location = mocked | |
} | |
} | |
private let locationManager = CLLocationManager() | |
private let geocoder = CLGeocoder() | |
private var systemStatus: CLAuthorizationStatus { | |
if #available(iOS 14, *) { | |
return locationManager.authorizationStatus | |
} else { | |
return CLLocationManager.authorizationStatus() | |
} | |
} | |
private var observations = [ObjectIdentifier: Observation]() | |
public private(set) lazy var authorizationStatus = systemStatus { | |
didSet { | |
if authorizationStatus != oldValue { | |
callObservers(with: authorizationStatus) | |
} | |
} | |
} | |
/// Location found by the CLLocationManager. | |
public private(set) var location: CLLocation? { | |
didSet { | |
/// - Note: There is iOS bug where `didUpdateLocations` is called multiple times | |
/// after `requestLocation` even though documentation states it's called only once | |
if let location = location { | |
callObservers(with: location) | |
updatePlacemark(with: location) | |
} | |
} | |
} | |
/// Placemark from reversing geocode of the location coordinates. | |
public private(set) var placemark: CLPlacemark? { | |
didSet { | |
if let placemark = placemark { | |
callObservers(with: placemark) | |
} | |
} | |
} | |
public var isServiceEnabled: Bool { | |
let enabled: [CLAuthorizationStatus] = [.authorizedAlways, .authorizedWhenInUse] | |
return enabled.contains(authorizationStatus) | |
} | |
public var isServiceDisabled: Bool { | |
let disabled: [CLAuthorizationStatus] = [.denied, .restricted] | |
return disabled.contains(authorizationStatus) | |
} | |
// MARK: Init | |
public override init() { | |
super.init() | |
locationManager.delegate = self | |
} | |
// MARK: API | |
public func requestAuthorization(_ type: AuthorizationType = .whenInUse) { | |
switch type { | |
case .whenInUse: | |
locationManager.requestWhenInUseAuthorization() | |
case .always: | |
locationManager.requestAlwaysAuthorization() | |
} | |
} | |
public func updateOnce(with accuracy: CLLocationAccuracy = kCLLocationAccuracyKilometer) { | |
locationManager.desiredAccuracy = accuracy | |
if isServiceEnabled { | |
locationManager.requestLocation() | |
} else { | |
resetData() | |
requestAuthorization() | |
} | |
} | |
public func addObserver(_ observer: AELocationObserver) { | |
let id = ObjectIdentifier(observer) | |
observations[id] = Observation(observer: observer) | |
} | |
public func removeObserver(_ observer: AELocationObserver) { | |
let id = ObjectIdentifier(observer) | |
observations.removeValue(forKey: id) | |
} | |
// MARK: Helpers | |
private func updatePlacemark(with location: CLLocation) { | |
geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, error in | |
if let placemark = placemarks?.first, error == nil { | |
self?.placemark = placemark | |
} else { | |
self?.placemark = nil | |
self?.callObservers(with: .reverseGeocodeFailed) | |
} | |
} | |
} | |
private func resetData() { | |
location = nil | |
placemark = nil | |
} | |
} | |
// MARK: - CLLocationManagerDelegate | |
@available(iOS 9.0, *) | |
extension AELocation: CLLocationManagerDelegate { | |
public func locationManager(_ manager: CLLocationManager, | |
didChangeAuthorization status: CLAuthorizationStatus) { | |
authorizationStatus = status | |
switch status { | |
case .authorizedAlways, .authorizedWhenInUse: | |
manager.requestLocation() | |
default: | |
manager.stopUpdatingLocation() | |
resetData() | |
callObservers(with: .locationServiceDisabled) | |
} | |
} | |
public func locationManager(_ manager: CLLocationManager, | |
didUpdateLocations locations: [CLLocation]) { | |
guard mocked == nil else { | |
location = mocked; return | |
} | |
if let location = locations.last { | |
self.location = location | |
} else { | |
resetData() | |
callObservers(with: .locationMissing) | |
} | |
} | |
public func locationManager(_ manager: CLLocationManager, | |
didFailWithError error: Swift.Error) { | |
resetData() | |
callObservers(with: .systemError(error)) | |
} | |
} | |
// MARK: - Observers | |
@available(iOS 9.0, *) | |
public protocol AELocationObserver: AnyObject { | |
func aeLocation(_ sender: AELocation, didUpdateStatus status: CLAuthorizationStatus) | |
func aeLocation(_ sender: AELocation, didUpdateLocation location: CLLocation) | |
func aeLocation(_ sender: AELocation, didUpdatePlacemark placemark: CLPlacemark) | |
func aeLocation(_ sender: AELocation, didFailWithError error: AELocation.Error) | |
} | |
@available(iOS 9.0, *) | |
public extension AELocationObserver { | |
func aeLocation(_ sender: AELocation, didUpdateStatus status: CLAuthorizationStatus) {} | |
func aeLocation(_ sender: AELocation, didUpdateLocation location: CLLocation) {} | |
func aeLocation(_ sender: AELocation, didUpdatePlacemark placemark: CLPlacemark) {} | |
func aeLocation(_ sender: AELocation, didFailWithError error: AELocation.Error) {} | |
} | |
@available(iOS 9.0, *) | |
private extension AELocation { | |
struct Observation { | |
weak var observer: AELocationObserver? | |
} | |
func callObservers(with authorizationStatus: CLAuthorizationStatus) { | |
for (id, observation) in observations { | |
guard let observer = observation.observer else { | |
observations.removeValue(forKey: id) | |
continue | |
} | |
observer.aeLocation(self, didUpdateStatus: authorizationStatus) | |
} | |
} | |
func callObservers(with location: CLLocation) { | |
for (id, observation) in observations { | |
guard let observer = observation.observer else { | |
observations.removeValue(forKey: id) | |
continue | |
} | |
observer.aeLocation(self, didUpdateLocation: location) | |
} | |
} | |
func callObservers(with placemark: CLPlacemark) { | |
for (id, observation) in observations { | |
guard let observer = observation.observer else { | |
observations.removeValue(forKey: id) | |
continue | |
} | |
observer.aeLocation(self, didUpdatePlacemark: placemark) | |
} | |
} | |
func callObservers(with error: AELocation.Error) { | |
for (id, observation) in observations { | |
guard let observer = observation.observer else { | |
observations.removeValue(forKey: id) | |
continue | |
} | |
observer.aeLocation(self, didFailWithError: error) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment