Skip to content

Instantly share code, notes, and snippets.

@tadija
Last active October 5, 2021 12:34
Show Gist options
  • Save tadija/dcdf25983c0dbf410e430221f74b20df to your computer and use it in GitHub Desktop.
Save tadija/dcdf25983c0dbf410e430221f74b20df to your computer and use it in GitHub Desktop.
AELocation
/**
* 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