From 005d80c5317bb7fafcaeebafc3e29d6936cbd1a9 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Fri, 2 Jun 2023 10:37:19 +0300 Subject: [PATCH] feat: Added UserLocationFetcher helper that requests authorization and subscribes to user location updates --- CHANGELOG.md | 5 +- Package.swift | 2 +- .../Sources/AppleMapUISettings.swift | 20 ++ .../Sources/ApplePlacemarkManager.swift | 9 +- .../Sources/Helpers/UserLocationFetcher.swift | 187 ++++++++++++++++++ .../Sources/Managers/BaseMapManager.swift | 16 +- .../Sources/Managers/BaseMapUISettings.swift | 36 +++- TIMapUtils/TIMapUtils.podspec | 2 + .../Sources/EndpointCacheService.swift | 63 +++--- project-scripts/push_to_podspecs.sh | 4 + 10 files changed, 305 insertions(+), 39 deletions(-) create mode 100644 TIMapUtils/Sources/Helpers/UserLocationFetcher.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c44933..6947e08b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ### 1.46.0 -- **Added**: +- **Added**: `AsyncSingleValueStorage` and `SingleValueStorageAsyncWrapper` for async access to SingleValue storages wtih swift concurrency support +- **Added**: `BaseMapUISettings` used to configure map view of different providers + user location icon rendering for yandex maps +- **Added**: `UserLocationFetcher` helper that requests authorization and subscribes to user location updates +- **Update**: add `DEVELOPMENT_INSTALL` support for all podspecs and fix playground compilation issues ### 1.45.0 diff --git a/Package.swift b/Package.swift index 73f189c6..15078619 100644 --- a/Package.swift +++ b/Package.swift @@ -108,7 +108,7 @@ let package = Package( // MARK: - Maps .target(name: "TIMapUtils", - dependencies: [], + dependencies: ["TILogging"], path: "TIMapUtils/Sources", plugins: [.plugin(name: "TISwiftLintPlugin")]), diff --git a/TIAppleMapUtils/Sources/AppleMapUISettings.swift b/TIAppleMapUtils/Sources/AppleMapUISettings.swift index 36692165..adb3cf94 100644 --- a/TIAppleMapUtils/Sources/AppleMapUISettings.swift +++ b/TIAppleMapUtils/Sources/AppleMapUISettings.swift @@ -24,8 +24,28 @@ import TIMapUtils import MapKit open class AppleMapUISettings: BaseMapUISettings { + open class Defaults: BaseMapUISettings.Defaults { + public static var showCompassButton: Bool { + false + } + } + public var showCompassButton = false + public init(showUserLocation: Bool = Defaults.showUserLocation, + isZoomEnabled: Bool = Defaults.isZoomEnabled, + isTiltEnabled: Bool = Defaults.isTiltEnabled, + isRotationEnabled: Bool = Defaults.isRotationEnabled, + showCompassButton: Bool = Defaults.showCompassButton) { + + self.showCompassButton = showCompassButton + + super.init(showUserLocation: showUserLocation, + isZoomEnabled: isZoomEnabled, + isTiltEnabled: isTiltEnabled, + isRotationEnabled: isRotationEnabled) + } + override open func apply(to mapView: MKMapView) { super.apply(to: mapView) diff --git a/TIAppleMapUtils/Sources/ApplePlacemarkManager.swift b/TIAppleMapUtils/Sources/ApplePlacemarkManager.swift index b7167010..6374cb66 100644 --- a/TIAppleMapUtils/Sources/ApplePlacemarkManager.swift +++ b/TIAppleMapUtils/Sources/ApplePlacemarkManager.swift @@ -50,8 +50,13 @@ open class ApplePlacemarkManager: BaseItemPlacemarkManager Void + public typealias OnAuthSuccessCallback = (CLLocationManager) -> Void + public typealias OnAuthFailureCallback = (Failure) -> Void + + public var locationManager: CLLocationManager + public var accuracyRequest: AccuracyRequest + + public var authorized: Bool { + if #available(iOS 14.0, *) { + return isAuthorized(status: locationManager.authorizationStatus) + } else { + return isAuthorized(status: CLLocationManager.authorizationStatus()) + } + } + + var authorizedFullAccuracy: Bool { + if #available(iOS 14.0, *) { + switch locationManager.accuracyAuthorization { + case .fullAccuracy: + return true + + case .reducedAccuracy: + return false + + @unknown default: + assertionFailure("Unimplemented accuracyAuthorization case: \(locationManager.accuracyAuthorization)") + return true + } + } else { + return true + } + } + + public var authorizedAccuracy: Bool { + switch accuracyRequest { + case .default: + return true + + case .fullAccuracy: + return authorizedFullAccuracy + } + } + + public var authSuccessCallback: OnAuthSuccessCallback + public var authFailureCallback: OnAuthFailureCallback? + + public var locationCallback: LocationCallback? + + public var logger: ErrorLogger = DefaultOSLogErrorLogger(subsystem: "TIMapUtils", category: "UserLocationFetcher") + + public init(locationManager: CLLocationManager = CLLocationManager(), + accuracyRequest: AccuracyRequest = .default, + onSuccess: @escaping OnAuthSuccessCallback = { $0.requestLocation() }, + onFailure: OnAuthFailureCallback? = nil, + locationCallback: LocationCallback? = nil) { + + self.locationManager = locationManager + self.accuracyRequest = accuracyRequest + self.authSuccessCallback = onSuccess + self.authFailureCallback = onFailure + self.locationCallback = locationCallback + + super.init() + } + + open func requestLocationUpdates(onlyIfHasAccess: Bool = false) { + if authorized && authorizedAccuracy || !onlyIfHasAccess { + locationManager.delegate = self + } + } + + open func requestAuth(for manager: CLLocationManager) { + manager.requestWhenInUseAuthorization() + } + + open func isAuthorized(status: CLAuthorizationStatus) -> Bool { + switch status { + case .authorizedWhenInUse, .authorizedAlways: + return true + + case .restricted, .denied: + return false + + case .notDetermined: + return false + + @unknown default: + assertionFailure("Unimplemented authorizationStatus case: \(status))") + return true + } + } + + // MARK: - CLLocationManagerDelegate + + open func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + switch status { + case .notDetermined: + requestAuth(for: manager) + + case .restricted: + authFailureCallback?(.restricted) + + case .denied: + authFailureCallback?(.denied) + + case .authorizedAlways, .authorizedWhenInUse: + handleSuccessAuthorization(with: status, for: manager) + + @unknown default: + assertionFailure("Unimplemented authorizationStatus case: \(status))") + authSuccessCallback(manager) + } + } + + open func handleSuccessAuthorization(with status: CLAuthorizationStatus, for manager: CLLocationManager) { + if authorizedFullAccuracy { + authSuccessCallback(manager) + } else { + switch accuracyRequest { + case let .fullAccuracy(purposeKey): + if #available(iOS 14.0, *) { + locationManager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purposeKey) { [weak self] in + if $0 != nil { + self?.authFailureCallback?(.fullAccuracyDenied) + } else { + self?.authSuccessCallback(manager) + } + } + } else { + authSuccessCallback(manager) + } + + case .default: + authSuccessCallback(manager) + } + } + } + + open func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let locationCallback else { + return + } + + locations.forEach(locationCallback) + } + + open func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + logger.log(error: error, file: #file, line: #line) + } +} diff --git a/TIMapUtils/Sources/Managers/BaseMapManager.swift b/TIMapUtils/Sources/Managers/BaseMapManager.swift index bfadcb0e..9f719fec 100644 --- a/TIMapUtils/Sources/Managers/BaseMapManager.swift +++ b/TIMapUtils/Sources/Managers/BaseMapManager.swift @@ -43,6 +43,8 @@ open class BaseMapManager CUF.Update public typealias CameraUpdateOnClusterTap = (CUF.BoundingBox) -> CUF.Update + public typealias LocationCallback = (PM.Position) -> Void + private let placemarkManagerCreator: PlacemarkManagerCreator private let clusterPlacemarkManagerCreator: ClusterPlacemarkManagerCreator @@ -83,8 +85,12 @@ open class BaseMapManager: MapUISettings { - public var showUserLocation = true + open class Defaults { // swiftlint:disable:this convenience_type + public static var showUserLocation: Bool { + true + } - public var isZoomEnabled = true - public var isTiltEnabled = false - public var isRotationEnabled = false + public static var isZoomEnabled: Bool { + true + } - public init() { + public static var isTiltEnabled: Bool { + false + } + + public static var isRotationEnabled: Bool { + false + } + } + + public var showUserLocation: Bool + + public var isZoomEnabled: Bool + public var isTiltEnabled: Bool + public var isRotationEnabled: Bool + + public init(showUserLocation: Bool = Defaults.showUserLocation, + isZoomEnabled: Bool = Defaults.isZoomEnabled, + isTiltEnabled: Bool = Defaults.isTiltEnabled, + isRotationEnabled: Bool = Defaults.isRotationEnabled) { + + self.showUserLocation = showUserLocation + self.isZoomEnabled = isZoomEnabled + self.isTiltEnabled = isTiltEnabled + self.isRotationEnabled = isRotationEnabled } open func apply(to mapView: MapView) { diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index f33973d3..37c22f9e 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -19,4 +19,6 @@ Pod::Spec.new do |s| s.exclude_files = s.name + '/*.app' end + s.dependency 'TILogging', s.version.to_s + end diff --git a/TINetworkingCache/Sources/EndpointCacheService.swift b/TINetworkingCache/Sources/EndpointCacheService.swift index b3941513..7cc2d4c9 100644 --- a/TINetworkingCache/Sources/EndpointCacheService.swift +++ b/TINetworkingCache/Sources/EndpointCacheService.swift @@ -118,31 +118,10 @@ public struct EndpointCacheService: SingleValueStorage { return asyncCache.hasStoredValue { if $0 { - guard !cancellableBag.isCancelled else { - return - } - - asyncCache.getValue { - switch $0 { - case let .success(cachedValue): - completion(.success(cachedValue)) - - case .failure: - guard !cancellableBag.isCancelled else { - return - } - - requestClosure { - if case let .success(newValue) = $0 { - _ = store(value: newValue) - } - - completion($0) - } - .add(to: cancellableBag) - } - } - .add(to: cancellableBag) + fetchCached(from: asyncCache, + cancellableBag: cancellableBag, + requestClosure: requestClosure, + completion: completion) } else { requestClosure { if case let .success(newValue) = $0 { @@ -156,4 +135,38 @@ public struct EndpointCacheService: SingleValueStorage { } } } + + public func fetchCached(from asyncCache: AsyncStorage, + cancellableBag: BaseCancellableBag, + requestClosure: @escaping ContentRequestClosure, + completion: @escaping FetchContentCompletion) + where AsyncStorage.ValueType == Content { + + guard !cancellableBag.isCancelled else { + return + } + + asyncCache.getValue { + switch $0 { + case let .success(cachedValue): + completion(.success(cachedValue)) + + case .failure: + guard !cancellableBag.isCancelled else { + return + } + + requestClosure { + if case let .success(newValue) = $0 { + _ = store(value: newValue) + } + + completion($0) + } + .add(to: cancellableBag) + } + } + .add(to: cancellableBag) + } } diff --git a/project-scripts/push_to_podspecs.sh b/project-scripts/push_to_podspecs.sh index 5d5f3827..2bb93d07 100755 --- a/project-scripts/push_to_podspecs.sh +++ b/project-scripts/push_to_podspecs.sh @@ -15,4 +15,8 @@ for module_name in $(cat ${SRCROOT}/project-scripts/ordered_modules_list.txt); do bundle exec pod repo push https://git.svc.touchin.ru/TouchInstinct/Podspecs ${SRCROOT}/${module_name}/${module_name}.podspec "$@" --allow-warnings + + if [ $? -ne 0 ]; then + exit $? + fi done