diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b67ce3..6947e08b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 1.46.0 + +- **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 - **Added**: `SingleValueStorage` implementations + `AppInstallLifetimeSingleValueStorage` for automatically removing keychain items on app reinstall. diff --git a/Package.swift b/Package.swift index 18d25962..15078619 100644 --- a/Package.swift +++ b/Package.swift @@ -100,11 +100,22 @@ let package = Package( path: "TIMoyaNetworking/Sources", plugins: [.plugin(name: "TISwiftLintPlugin")]), - .target(name: "TINetworkingCache", dependencies: ["TIFoundationUtils", "TINetworking", "Cache"], path: "TINetworkingCache/Sources"), + .target(name: "TINetworkingCache", + dependencies: ["TIFoundationUtils", "TINetworking", "Cache"], + path: "TINetworkingCache/Sources", + plugins: [.plugin(name: "TISwiftLintPlugin")]), // MARK: - Maps - .target(name: "TIMapUtils", dependencies: [], path: "TIMapUtils/Sources"), - .target(name: "TIAppleMapUtils", dependencies: ["TIMapUtils"], path: "TIAppleMapUtils/Sources"), + + .target(name: "TIMapUtils", + dependencies: ["TILogging"], + path: "TIMapUtils/Sources", + plugins: [.plugin(name: "TISwiftLintPlugin")]), + + .target(name: "TIAppleMapUtils", + dependencies: ["TIMapUtils"], + path: "TIAppleMapUtils/Sources", + plugins: [.plugin(name: "TISwiftLintPlugin")]), // MARK: - Elements .target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"), diff --git a/README.md b/README.md index 9b4f48f6..85e3087c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ target 'TIModuleName' do use_frameworks! pod 'TIDependencyModuleName', :path => '../../../../TIDependencyModuleName/TIDependencyModuleName.podspec' + pod 'TIModuleName', :path => '../../../../TIModuleName/TIModuleName.podspec' end" > PlaygroundPodfile $ nef playground --name TIModuleName --cocoapods --custom-podfile PlaygroundPodfile diff --git a/TIAppleMapUtils/Sources/AppleMapManager.swift b/TIAppleMapUtils/Sources/AppleMapManager.swift index e4994236..2afd1c8b 100644 --- a/TIAppleMapUtils/Sources/AppleMapManager.swift +++ b/TIAppleMapUtils/Sources/AppleMapManager.swift @@ -24,9 +24,10 @@ import TIMapUtils import MapKit open class AppleMapManager: BaseMapManager, - AppleClusterPlacemarkManager, - MKCameraUpdate> { + ApplePlacemarkManager, + AppleClusterPlacemarkManager, + MKCameraUpdate, + AppleMapUISettings> { public typealias ClusteringIdentifier = String diff --git a/TIAppleMapUtils/Sources/AppleMapUISettings.swift b/TIAppleMapUtils/Sources/AppleMapUISettings.swift new file mode 100644 index 00000000..adb3cf94 --- /dev/null +++ b/TIAppleMapUtils/Sources/AppleMapUISettings.swift @@ -0,0 +1,58 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +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) + + mapView.showsUserLocation = showUserLocation + mapView.isZoomEnabled = isZoomEnabled + mapView.isPitchEnabled = isTiltEnabled + mapView.isRotateEnabled = isRotationEnabled + mapView.showsCompass = showCompassButton + } +} 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 'MIT', :file => 'LICENSE' } @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.dependency 'TIMapUtils', s.version.to_s end diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index d9783051..fccbffab 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Login, registration, confirmation and other related actions' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '13.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.dependency 'TIFoundationUtils', s.version.to_s s.dependency 'TIKeychainUtils', s.version.to_s diff --git a/TIDeepLink/PlaygroundPodfile b/TIDeepLink/PlaygroundPodfile index 94c744c3..c2add1b9 100644 --- a/TIDeepLink/PlaygroundPodfile +++ b/TIDeepLink/PlaygroundPodfile @@ -6,4 +6,5 @@ target 'TIDeeplink' do pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TIDeeplink', :path => '../../../../TIDeeplink/TIDeeplink.podspec' end diff --git a/TIDeepLink/TIDeeplink.app/Contents/MacOS/Podfile b/TIDeepLink/TIDeeplink.app/Contents/MacOS/Podfile index 94c744c3..c2add1b9 100644 --- a/TIDeepLink/TIDeeplink.app/Contents/MacOS/Podfile +++ b/TIDeepLink/TIDeeplink.app/Contents/MacOS/Podfile @@ -6,4 +6,5 @@ target 'TIDeeplink' do pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TIDeeplink', :path => '../../../../TIDeeplink/TIDeeplink.podspec' end diff --git a/TIDeeplink/TIDeeplink.podspec b/TIDeeplink/TIDeeplink.podspec index 2584a307..14e443fe 100644 --- a/TIDeeplink/TIDeeplink.podspec +++ b/TIDeeplink/TIDeeplink.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeeplink' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Deeplink service API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIDeveloperUtils/TIDeveloperUtils.podspec b/TIDeveloperUtils/TIDeveloperUtils.podspec index 688d67f9..325b91ec 100644 --- a/TIDeveloperUtils/TIDeveloperUtils.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeveloperUtils' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -11,7 +11,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.dependency 'TIUIKitCore', s.version.to_s s.dependency 'TIUIElements', s.version.to_s diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index d843966c..a4726845 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Cart, products, promocodes, bonuses and other related actions' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIFoundationUtils/DataStorage/Sources/AsyncSingleValueStorage/AsyncSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/AsyncSingleValueStorage/AsyncSingleValueStorage.swift new file mode 100644 index 00000000..3e1759d5 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/AsyncSingleValueStorage/AsyncSingleValueStorage.swift @@ -0,0 +1,58 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol AsyncSingleValueStorage { + associatedtype ValueType + associatedtype ErrorType: Error + + func hasStoredValue(completion: @escaping (Bool) -> Void) -> Cancellable + func store(value: ValueType, completion: @escaping (Result) -> Void) -> Cancellable + func getValue(completion: @escaping (Result) -> Void) -> Cancellable + func deleteValue(completion: @escaping (Result) -> Void) -> Cancellable +} + +@available(iOS 13.0.0, *) +public extension AsyncSingleValueStorage { + func hasStoredValue() async -> Bool { + await withTaskCancellableClosure { + hasStoredValue(completion: $0) + } + } + + func store(value: ValueType) async -> Result { + await withTaskCancellableClosure { + store(value: value, completion: $0) + } + } + + func getValue() async -> Result { + await withTaskCancellableClosure { + getValue(completion: $0) + } + } + + func deleteValue() async -> Result { + await withTaskCancellableClosure { + deleteValue(completion: $0) + } + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/AsyncSingleValueStorage/SingleValueStorageAsyncWrapper.swift b/TIFoundationUtils/DataStorage/Sources/AsyncSingleValueStorage/SingleValueStorageAsyncWrapper.swift new file mode 100644 index 00000000..7260dbe0 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/AsyncSingleValueStorage/SingleValueStorageAsyncWrapper.swift @@ -0,0 +1,65 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Dispatch + +public struct SingleValueStorageAsyncWrapper: AsyncSingleValueStorage { + private let wrappedStorage: Storage + private let accessQueue: DispatchQueue + + public init(wrappedStorage: Storage, accessQueue: DispatchQueue) { + self.wrappedStorage = wrappedStorage + self.accessQueue = accessQueue + } + + public func hasStoredValue(completion: @escaping (Bool) -> Void) -> Cancellable { + withWorkItem(execute: wrappedStorage.hasStoredValue, completion: completion) + } + + public func store(value: Storage.ValueType, completion: @escaping (Result) -> Void) -> Cancellable { + withWorkItem(execute: { wrappedStorage.store(value: value) }, completion: completion) + } + + public func getValue(completion: @escaping (Result) -> Void) -> Cancellable { + withWorkItem(execute: wrappedStorage.getValue, completion: completion) + } + + public func deleteValue(completion: @escaping (Result) -> Void) -> Cancellable { + withWorkItem(execute: wrappedStorage.deleteValue, completion: completion) + } + + private func withWorkItem(execute closure: @escaping () -> T, completion: @escaping (T) -> Void) -> Cancellable { + let workItem = DispatchWorkItem { + completion(closure()) + } + + accessQueue.async(execute: workItem) + + return workItem + } +} + +public extension SingleValueStorage { + func async(on queue: DispatchQueue) -> SingleValueStorageAsyncWrapper { + SingleValueStorageAsyncWrapper(wrappedStorage: self, accessQueue: queue) + } +} diff --git a/TIFoundationUtils/PlaygroundPodfile b/TIFoundationUtils/PlaygroundPodfile index 92fd45c2..95c2fd95 100644 --- a/TIFoundationUtils/PlaygroundPodfile +++ b/TIFoundationUtils/PlaygroundPodfile @@ -1,7 +1,7 @@ ENV["DEVELOPMENT_INSTALL"] = "true" target 'TIFoundationUtils' do - platform :ios, 10.0 + platform :ios, 11.0 use_frameworks! pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile index 92fd45c2..95c2fd95 100644 --- a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile @@ -1,7 +1,7 @@ ENV["DEVELOPMENT_INSTALL"] = "true" target 'TIFoundationUtils' do - platform :ios, 10.0 + platform :ios, 11.0 use_frameworks! pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 3be78475..d70cc0cb 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Set of helpers for Foundation framework classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIGoogleMapUtils/Sources/GoogleMapManager.swift b/TIGoogleMapUtils/Sources/GoogleMapManager.swift index 47ed240a..f3427fed 100644 --- a/TIGoogleMapUtils/Sources/GoogleMapManager.swift +++ b/TIGoogleMapUtils/Sources/GoogleMapManager.swift @@ -24,9 +24,10 @@ import TIMapUtils import GoogleMaps open class GoogleMapManager: BaseMapManager, - GoogleClusterPlacemarkManager, - GMSCameraUpdate> { + GooglePlacemarkManager, + GoogleClusterPlacemarkManager, + GMSCameraUpdate, + GoogleMapUISettings> { public init(map: GMSMapView, positionGetter: @escaping PositionGetter, diff --git a/TIGoogleMapUtils/Sources/GoogleMapUISettings.swift b/TIGoogleMapUtils/Sources/GoogleMapUISettings.swift new file mode 100644 index 00000000..7ad90770 --- /dev/null +++ b/TIGoogleMapUtils/Sources/GoogleMapUISettings.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIMapUtils +import GoogleMaps + +open class GoogleMapUISettings: BaseMapUISettings { + public var showMyLocationButton = true + + open override func apply(to mapView: GMSMapView) { + super.apply(to: mapView) + + mapView.isMyLocationEnabled = showUserLocation + + mapView.settings.zoomGestures = isZoomEnabled + mapView.settings.tiltGestures = isTiltEnabled + mapView.settings.rotateGestures = isRotationEnabled + mapView.settings.myLocationButton = showMyLocationButton + } +} diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index a5e1af69..10448203 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '12.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.static_framework = true s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/BaseSingleValueKeychainStorage.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/BaseSingleValueKeychainStorage.swift index 0641a4df..e52370f5 100644 --- a/TIKeychainUtils/Sources/SingleValueKeychainStorage/BaseSingleValueKeychainStorage.swift +++ b/TIKeychainUtils/Sources/SingleValueKeychainStorage/BaseSingleValueKeychainStorage.swift @@ -31,12 +31,12 @@ open class BaseSingleValueKeychainStorage: BaseSingleValueStorage 'MIT', :file => 'LICENSE' } @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.dependency 'TIFoundationUtils', s.version.to_s s.dependency 'KeychainAccess', "~> 4.2" diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index d47e273a..3d6a2382 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TILogging' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Logging for TI libraries.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -10,6 +10,13 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end end diff --git a/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift index 797593fd..1cb62540 100644 --- a/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift +++ b/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift @@ -23,19 +23,19 @@ import UIKit public struct BorderDrawingOperation: DrawingOperation { - public var frameableContentSize: CGSize + public var frameableContentRect: CGRect public var border: CGFloat public var color: CGColor public var radius: CGFloat public var exteriorBorder: Bool - public init(frameableContentSize: CGSize, + public init(frameableContentRect: CGRect, border: CGFloat, color: CGColor, radius: CGFloat, exteriorBorder: Bool) { - self.frameableContentSize = frameableContentSize + self.frameableContentRect = frameableContentRect self.border = border self.color = color self.radius = radius @@ -47,10 +47,10 @@ public struct BorderDrawingOperation: DrawingOperation { public func affectedArea(in context: CGContext?) -> CGRect { let margin = exteriorBorder ? border : 0 - let width = frameableContentSize.width + margin * 2 - let height = frameableContentSize.height + margin * 2 + let width = frameableContentRect.width + margin * 2 + let height = frameableContentRect.height + margin * 2 - return CGRect(origin: .zero, + return CGRect(origin: frameableContentRect.origin, size: CGSize(width: width, height: height)) } @@ -59,7 +59,8 @@ public struct BorderDrawingOperation: DrawingOperation { let ctxSize = drawArea.size.ceiledContextSize - let ctxRect = CGRect(origin: .zero, size: CGSize(width: ctxSize.width, height: ctxSize.height)) + let ctxRect = CGRect(origin: frameableContentRect.origin, + size: CGSize(width: ctxSize.width, height: ctxSize.height)) let widthDiff = CGFloat(ctxSize.width) - drawArea.width // difference between context width and real width let heightDiff = CGFloat(ctxSize.height) - drawArea.height // difference between context height and real height diff --git a/TIMapUtils/Sources/Drawing/Operations/CurrentLocationDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/CurrentLocationDrawingOperation.swift new file mode 100644 index 00000000..334bee2b --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Operations/CurrentLocationDrawingOperation.swift @@ -0,0 +1,163 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import CoreGraphics + +public struct CurrentLocationDrawingOperation: DrawingOperation { + public enum Defaults { + public static var iconSize: CGSize { + CGSize(width: 27, height: 27) + } + + public static var borderWidth: CGFloat { + 2 + } + } + + public var iconSize: CGSize + public var borderWidth: CGFloat + public var mainColor: CGColor + public var borderColor: CGColor + public var backgroundColor: CGColor + public var showHeadingArrow: Bool + + private var innerCircleSize: CGSize { + CGSize(width: innerBorderSize.width - borderWidth, + height: innerBorderSize.height - borderWidth) + } + + private var innerBorderSize: CGSize { + CGSize(width: outerBorderSize.width - borderWidth, + height: outerBorderSize.height - borderWidth) + } + + private var outerBorderSize: CGSize { + CGSize(width: iconSize.width - 8, + height: iconSize.height - 8) + } + + private var innerCircleOrigin: CGPoint { + CGPoint(x: (iconSize.width - innerCircleSize.width) / 2, + y: (iconSize.height - innerCircleSize.height) / 2) + } + + private var innerBorderOrigin: CGPoint { + CGPoint(x: (iconSize.width - innerBorderSize.width) / 2, + y: (iconSize.height - innerBorderSize.height) / 2) + } + + private var outerBorderOrigin: CGPoint { + CGPoint(x: (iconSize.width - outerBorderSize.width) / 2, + y: (iconSize.height - outerBorderSize.height) / 2) + } + + private var iconRect: CGRect { + CGRect(origin: .zero, + size: iconSize) + } + + private var innerCircleRect: CGRect { + CGRect(origin: innerCircleOrigin, + size: innerCircleSize) + } + + private var innerBorderRect: CGRect { + CGRect(origin: innerBorderOrigin, + size: innerBorderSize) + } + + private var outerBorderRect: CGRect { + CGRect(origin: outerBorderOrigin, + size: outerBorderSize) + } + + public init(iconSize: CGSize = Defaults.iconSize, + borderWidth: CGFloat = Defaults.borderWidth, + mainColor: CGColor, + borderColor: CGColor, + showHeadingArrow: Bool = true) { + + self.iconSize = iconSize + self.borderWidth = borderWidth + self.mainColor = mainColor + self.borderColor = borderColor + self.backgroundColor = mainColor.copy(alpha: 0.3) ?? mainColor + self.showHeadingArrow = showHeadingArrow + } + + public func affectedArea(in context: CGContext? = nil) -> CGRect { + iconRect + } + + public func apply(in context: CGContext) { + let backgroundDrawing = SolidFillDrawingOperation(color: backgroundColor, + ellipseRect: iconRect) + + let innerCircleDrawing = SolidFillDrawingOperation(color: mainColor, + ellipseRect: innerCircleRect) + + let innerBorderDrawing = BorderDrawingOperation(frameableContentRect: innerBorderRect, + border: borderWidth, + color: borderColor, + radius: min(innerBorderSize.width, innerBorderSize.height) / 2, + exteriorBorder: false) + + let outerBorderDrawing = BorderDrawingOperation(frameableContentRect: outerBorderRect, + border: borderWidth, + color: backgroundColor, + radius: min(outerBorderSize.width, outerBorderSize.height) / 2, + exteriorBorder: false) + + // flip horizontally + + context.translateBy(x: 0, y: iconSize.height) + context.scaleBy(x: 1, y: -1) + + backgroundDrawing.apply(in: context) + + context.setFillColor(mainColor) + context.setLineWidth(.zero) + + guard showHeadingArrow else { + innerCircleDrawing.apply(in: context) + + innerBorderDrawing.apply(in: context) + + return + } + + context.addLines(between: [ + CGPoint(x: innerBorderRect.minX, y: innerBorderRect.midY), + CGPoint(x: iconRect.midX, y: iconRect.maxY), + CGPoint(x: innerBorderRect.maxX, y: innerBorderRect.midY) + ]) + + context.drawPath(using: .fillStroke) + + innerCircleDrawing.apply(in: context) + + innerBorderDrawing.apply(in: context) + + context.setBlendMode(.destinationAtop) + outerBorderDrawing.apply(in: context) + } +} diff --git a/TIMapUtils/Sources/Helpers/UserLocationFetcher.swift b/TIMapUtils/Sources/Helpers/UserLocationFetcher.swift new file mode 100644 index 00000000..b39ac634 --- /dev/null +++ b/TIMapUtils/Sources/Helpers/UserLocationFetcher.swift @@ -0,0 +1,187 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import CoreLocation +import TILogging + +open class UserLocationFetcher: NSObject, CLLocationManagerDelegate { + public enum AccuracyRequest { + case `default` + case fullAccuracy(purposeKey: String) + } + + public enum Failure: Error { + case restricted + case denied + case fullAccuracyDenied + } + + public typealias LocationCallback = (CLLocation) -> 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/IconProviders/DefaultClusterIconRenderer.swift b/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift index 08f50e73..65044aaf 100644 --- a/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift +++ b/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift @@ -113,7 +113,7 @@ open class DefaultClusterIconRenderer { open func borderDrawingOperation(iconSize: CGSize, cornerRadius: CGFloat) -> DrawingOperation { - BorderDrawingOperation(frameableContentSize: iconSize, + BorderDrawingOperation(frameableContentRect: CGRect(origin: .zero, size: iconSize), border: borderWidth, color: border.color.cgColor, radius: cornerRadius, diff --git a/TIMapUtils/Sources/Managers/BaseMapManager.swift b/TIMapUtils/Sources/Managers/BaseMapManager.swift index 43345bf0..9f719fec 100644 --- a/TIMapUtils/Sources/Managers/BaseMapManager.swift +++ b/TIMapUtils/Sources/Managers/BaseMapManager.swift @@ -26,10 +26,12 @@ import UIKit.UIGeometry open class BaseMapManager where PM.Position: LocationCoordinate, - PM.Position == CUF.Position, - CUF.Update.Map == Map, - CUF.BoundingBox == CPM.Position { + CUF: CameraUpdateFactory, + MS: MapUISettings> where PM.Position: LocationCoordinate, + PM.Position == CUF.Position, + CUF.Update.Map == Map, + CUF.BoundingBox == CPM.Position, + MS.MapView == Map { public typealias PositionGetter = (PM.DataModel) -> PM.Position? @@ -41,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 @@ -55,6 +59,8 @@ open class BaseMapManager: MapUISettings { + open class Defaults { // swiftlint:disable:this convenience_type + public static var showUserLocation: Bool { + true + } + + public static var isZoomEnabled: Bool { + true + } + + 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) { + // override in subclass + } +} diff --git a/TIMapUtils/Sources/Managers/MapUISettings.swift b/TIMapUtils/Sources/Managers/MapUISettings.swift new file mode 100644 index 00000000..1a1c99a2 --- /dev/null +++ b/TIMapUtils/Sources/Managers/MapUISettings.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol MapUISettings: AnyObject { + associatedtype MapView + + var isZoomEnabled: Bool { get set } + var isTiltEnabled: Bool { get set } + var isRotationEnabled: Bool { get set } + + var showUserLocation: Bool { get set } + + func apply(to mapView: MapView) +} diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index 2ce197c2..37c22f9e 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Set of helpers for map objects clustering and interacting.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -10,6 +10,15 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end + + s.dependency 'TILogging', s.version.to_s end diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index 7c885bdd..a640216c 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Moya + Swagger network service.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/**/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.dependency 'TINetworking', s.version.to_s s.dependency 'TIFoundationUtils', s.version.to_s diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index b00f90fd..02071b65 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Swagger-frendly networking layer helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.dependency 'TIFoundationUtils', s.version.to_s s.dependency 'TILogging', s.version.to_s diff --git a/TINetworkingCache/Sources/EndpointCacheService.swift b/TINetworkingCache/Sources/EndpointCacheService.swift index ae0a50ca..7cc2d4c9 100644 --- a/TINetworkingCache/Sources/EndpointCacheService.swift +++ b/TINetworkingCache/Sources/EndpointCacheService.swift @@ -25,31 +25,15 @@ import TIFoundationUtils import Cache import Foundation -public struct EndpointCacheService { +public struct EndpointCacheService: SingleValueStorage { private let serializedRequest: SerializedRequest private let multiLevelStorage: Storage - public var cachedContent: Content? { - get { - guard let entry = try? multiLevelStorage.entry(forKey: serializedRequest), !entry.expiry.isExpired else { - try? multiLevelStorage.removeObject(forKey: serializedRequest) - return nil - } - - return entry.object - } - nonmutating set { - if let object = newValue { - try? multiLevelStorage.setObject(object, - forKey: serializedRequest) - } else { - try? multiLevelStorage.removeObject(forKey: serializedRequest) - } - } - } + public typealias ContentRequestClosure = (@escaping (Swift.Result) -> Void) -> Cancellable + public typealias FetchContentCompletion = (Swift.Result) -> Void public init(serializedRequest: SerializedRequest, - cacheLifetime: TimeInterval, + cacheLifetime: Expiry, jsonCodingConfigurator: JsonCodingConfigurator) throws { self.serializedRequest = serializedRequest @@ -63,8 +47,8 @@ public struct EndpointCacheService { } let diskConfig = DiskConfig(name: nameWithoutLeadingSlash, - expiry: .seconds(cacheLifetime)) - let memoryConfig = MemoryConfig(expiry: .seconds(cacheLifetime), + expiry: cacheLifetime) + let memoryConfig = MemoryConfig(expiry: cacheLifetime, countLimit: 0, totalCostLimit: 0) @@ -78,4 +62,111 @@ public struct EndpointCacheService { memoryConfig: memoryConfig, transformer: transformer) } + + // MARK: - SingleValueStorage + + public func hasStoredValue() -> Bool { + do { + return try multiLevelStorage.existsObject(forKey: serializedRequest) && + !(try multiLevelStorage.isExpiredObject(forKey: serializedRequest)) + } catch { + return false + } + } + + public func store(value: Content) -> Swift.Result { + do { + return .success(try multiLevelStorage.setObject(value, forKey: serializedRequest)) + } catch let storageError as Cache.StorageError { + return .failure(storageError) + } catch { + return .failure(.decodingFailed) + } + } + + public func getValue() -> Swift.Result { + guard hasStoredValue() else { + return .failure(.notFound) + } + do { + return .success(try multiLevelStorage.object(forKey: serializedRequest)) + } catch let storageError as Cache.StorageError { + return .failure(storageError) + } catch { + return .failure(.encodingFailed) + } + } + + public func deleteValue() -> Swift.Result { + do { + return .success(try multiLevelStorage.removeObject(forKey: serializedRequest)) + } catch let storageError as Cache.StorageError { + return .failure(storageError) + } catch { + return .failure(.notFound) + } + } + + public func async() -> SingleValueStorageAsyncWrapper { + async(on: multiLevelStorage.async.serialQueue) + } + + public func getCachedOrRequest(from requestClosure: @escaping ContentRequestClosure, + completion: @escaping FetchContentCompletion) -> Cancellable { + Cancellables.scoped { cancellableBag in + let asyncCache = async() + + return asyncCache.hasStoredValue { + if $0 { + fetchCached(from: asyncCache, + cancellableBag: cancellableBag, + requestClosure: requestClosure, + completion: completion) + } else { + requestClosure { + if case let .success(newValue) = $0 { + _ = store(value: newValue) + } + + completion($0) + } + .add(to: cancellableBag) + } + } + } + } + + 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/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index a66652cc..de220e82 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Caching results of EndpointRequests.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.dependency 'TIFoundationUtils', s.version.to_s s.dependency 'TINetworking', s.version.to_s diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index 180d8c99..685cf69c 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Generic pagination component.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index 32478aa0..cdba18ff 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '13.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.dependency 'TIUIKitCore', s.version.to_s s.dependency 'TISwiftUtils', s.version.to_s diff --git a/TISwiftUtils/Sources/Helpers/Typealias.swift b/TISwiftUtils/Sources/Helpers/Typealias.swift index e85d9ebc..c9594982 100644 --- a/TISwiftUtils/Sources/Helpers/Typealias.swift +++ b/TISwiftUtils/Sources/Helpers/Typealias.swift @@ -20,6 +20,10 @@ // THE SOFTWARE. // +// swiftlint:disable parameter_closure + +// MARK: - Basic closures + /// Closure with custom arguments and return value. public typealias Closure = (Input) -> Output @@ -29,7 +33,7 @@ public typealias ResultClosure = () -> Output /// Closure that takes custom arguments and returns Void. public typealias ParameterClosure = Closure -// MARK: Throwable versions +// MARK: - Throwable versions /// Closure with custom arguments and return value, may throw an error. public typealias ThrowableClosure = (Input) throws -> Output @@ -40,7 +44,7 @@ public typealias ThrowableResultClosure = () throws -> Output /// Closure that takes custom arguments and returns Void, may throw an error. public typealias ThrowableParameterClosure = ThrowableClosure -// MARK: Concrete closures +// MARK: - Concrete closures /// Closure that takes no arguments and returns Void. public typealias VoidClosure = ResultClosure @@ -48,6 +52,7 @@ public typealias VoidClosure = ResultClosure /// Closure that takes no arguments, may throw an error and returns Void. public typealias ThrowableVoidClosure = () throws -> Void +// MARK: - Async versions /// Async closure with custom arguments and return value. public typealias AsyncClosure = (Input) async -> Output @@ -58,7 +63,7 @@ public typealias AsyncResultClosure = () async -> Output /// Async closure that takes custom arguments and returns Void. public typealias AsyncParameterClosure = AsyncClosure -// MARK: Async throwable versions +// MARK: - Async throwable versions /// Async closure with custom arguments and return value, may throw an error. public typealias ThrowableAsyncClosure = (Input) async throws -> Output @@ -69,10 +74,72 @@ public typealias ThrowableAsyncResultClosure = () async throws -> Output /// Async closure that takes custom arguments and returns Void, may throw an error. public typealias ThrowableAsyncParameterClosure = ThrowableAsyncClosure -// MARK: Async concrete closures +// MARK: - Async concrete closures /// Async closure that takes no arguments and returns Void. public typealias AsyncVoidClosure = AsyncResultClosure /// Async closure that takes no arguments, may throw an error and returns Void. public typealias ThrowableAsyncVoidClosure = () async throws -> Void + +// MARK: - @MainActor basic closures + +/// @MainActor closure with custom arguments and return value. +public typealias UIClosure = @MainActor (Input) -> Output + +/// @MainActor closure with no arguments and custom return value. +public typealias UIResultClosure = @MainActor () -> Output + +/// @MainActior closure that takes custom arguments and returns Void. +public typealias UIParameterClosure = UIClosure + +// MARK: - Throwable @MainActor versions + +/// @MainActor closure with custom arguments and return value, may throw an error. +public typealias UIThrowableClosure = @MainActor (Input) throws -> Output + +/// @MainActor closure with no arguments and custom return value, may throw an error. +public typealias UIThrowableResultClosure = @MainActor () throws -> Output + +/// @MainActior closure that takes custom arguments and returns Void, may throw an error. +public typealias UIThrowableParameterClosure = UIThrowableClosure + +// MARK: - Concrete @MainActor closures + +/// @MainActior closure that takes no arguments and returns Void. +public typealias UIVoidClosure = UIResultClosure + +/// @MainActior closure that takes no arguments, may throw an error and returns Void. +public typealias UIThrowableVoidClosure = @MainActor () throws -> Void + +/// Async @MainActor closure with custom arguments and return value. +public typealias UIAsyncClosure = @MainActor (Input) async -> Output + +// MARK: - @MainActor async basic closures + +/// Async @MainActor closure with no arguments and custom return value. +public typealias UIAsyncResultClosure = @MainActor () async -> Output + +/// Async @MainActior closure that takes custom arguments and returns Void. +public typealias UIAsyncParameterClosure = AsyncClosure + +// MARK: - @MainActor async throwable versions + +/// Async @MainActor closure with custom arguments and return value, may throw an error. +public typealias UIThrowableAsyncClosure = @MainActor (Input) async throws -> Output + +/// Async @MainActor closure with no arguments and custom return value, may throw an error. +public typealias UIThrowableAsyncResultClosure = @MainActor () async throws -> Output + +/// Async @MainActior closure that takes custom arguments and returns Void, may throw an error. +public typealias UIThrowableAsyncParameterClosure = UIThrowableAsyncClosure + +// MARK: - @MainActor async concrete closures + +/// Async @MainActior closure that takes no arguments and returns Void. +public typealias UIAsyncVoidClosure = UIAsyncResultClosure + +/// Async @MainActior closure that takes no arguments, may throw an error and returns Void. +public typealias UIThrowableAsyncVoidClosure = @MainActor () async throws -> Void + +// swiftlint:enable parameter_closure diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index a8c15a3c..de1784b6 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Bunch of useful helpers for Swift development.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index 8b3eee32..250c7a03 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITextProcessing/PlaygroundPodfile b/TITextProcessing/PlaygroundPodfile index c65e8908..c7334ab4 100644 --- a/TITextProcessing/PlaygroundPodfile +++ b/TITextProcessing/PlaygroundPodfile @@ -1,7 +1,8 @@ ENV["DEVELOPMENT_INSTALL"] = "true" target 'TITextProcessing' do - platform :ios, 10 + platform :ios, 11 use_frameworks! + pod 'TITextProcessing', :path => '../../../../TITextProcessing/TITextProcessing.podspec' end diff --git a/TITextProcessing/TITextProcessing.app/Contents/MacOS/Podfile b/TITextProcessing/TITextProcessing.app/Contents/MacOS/Podfile index c65e8908..c7334ab4 100644 --- a/TITextProcessing/TITextProcessing.app/Contents/MacOS/Podfile +++ b/TITextProcessing/TITextProcessing.app/Contents/MacOS/Podfile @@ -1,7 +1,8 @@ ENV["DEVELOPMENT_INSTALL"] = "true" target 'TITextProcessing' do - platform :ios, 10 + platform :ios, 11 use_frameworks! + pod 'TITextProcessing', :path => '../../../../TITextProcessing/TITextProcessing.podspec' end diff --git a/TITextProcessing/TITextProcessing.app/Contents/MacOS/TITextProcessing.playground/Pages/TITextProcessing.xcplaygroundpage/Contents.swift b/TITextProcessing/TITextProcessing.app/Contents/MacOS/TITextProcessing.playground/Pages/TITextProcessing.xcplaygroundpage/Contents.swift index 1701d733..d888a10c 100644 --- a/TITextProcessing/TITextProcessing.app/Contents/MacOS/TITextProcessing.playground/Pages/TITextProcessing.xcplaygroundpage/Contents.swift +++ b/TITextProcessing/TITextProcessing.app/Contents/MacOS/TITextProcessing.playground/Pages/TITextProcessing.xcplaygroundpage/Contents.swift @@ -22,9 +22,9 @@ import Foundation import TITextProcessing -let textFormatter = TextFormatter(regex: "(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})") +let cardNumberTextFormatter = TextFormatter(regex: "(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})") -print(textFormatter.getRegexReplacement()) +print(cardNumberTextFormatter.getRegexReplacement()) /* Выведет в консоль: @@ -40,12 +40,7 @@ print(textFormatter.getRegexReplacement()) **Output**: `1234 5678 9012 3456` */ -import Foundation -import TITextProcessing - -let textFormatter = TextFormatter(regex: "(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})") - -print(textFormatter.getRegexPlaceholder()) +print(cardNumberTextFormatter.getRegexPlaceholder()) /* Выведет в консоль: @@ -63,12 +58,7 @@ print(textFormatter.getRegexPlaceholder()) > P.S. Учитываем, что `TextFormatter` был проинициализирован со слеюущим регулярным выражением: `(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})` */ -import Foundation -import TITextProcessing - -let textFormatter = TextFormatter(regex: "(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})") - -print(textFormatter.getFormattedText("2200111555550080")) +print(cardNumberTextFormatter.getFormattedText("2200111555550080")) /* Выведет в консоль: @@ -85,9 +75,6 @@ print(textFormatter.getFormattedText("2200111555550080")) Функция, преобразующий входящее регулярное выражение в структуру, содержащую шаблон подстановки и матрицу символов, например: */ -import Foundation -import TITextProcessing - let replaceGenerator: RegexReplaceGenerator = DefaultRegexReplaceGenerator() let item = replaceGenerator.generateReplacement(for: "(\\d{2})\\/?(\\d{2})") @@ -126,9 +113,6 @@ print(item.matrixOfSymbols) Функция, преобразующая входящую матрицу символов в текст-заполнитель, например: */ -import Foundation -import TITextProcessing - let matrix: [[Character]] = [ ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], @@ -151,9 +135,6 @@ print(placeholder) ## - Примеры использования: */ -import Foundation -import TITextProcessing - // MARK: - Форматирование даты let dateRegex = "(\\d{2})\\/?(\\d{2})" diff --git a/TITextProcessing/TITextProcessing.podspec b/TITextProcessing/TITextProcessing.podspec index 2ed02fb1..3e60037e 100644 --- a/TITextProcessing/TITextProcessing.podspec +++ b/TITextProcessing/TITextProcessing.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITextProcessing' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'A text processing service helping to get a text mask and a placeholder from incoming regex.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -10,13 +10,13 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - sources = '/Sources/**/*' + sources = 'Sources/**/*' if ENV["DEVELOPMENT_INSTALL"] # installing using :path => s.source_files = sources s.exclude_files = s.name + '.app' else - s.source_files = s.name + sources + s.source_files = s.name + '/' + sources s.exclude_files = s.name + '/*.app' end diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index 7866a8e6..8bfbd184 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -11,12 +11,12 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - sources = '/Sources/**/*' + sources = 'Sources/**/*' if ENV["DEVELOPMENT_INSTALL"] # installing using :path => s.source_files = sources s.exclude_files = s.name + '.app' else - s.source_files = s.name + sources + s.source_files = s.name + '/' + sources s.exclude_files = s.name + '/*.app' end diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index dbe86d0c..b0a5799e 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -11,7 +11,15 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end + s.framework = 'UIKit' s.dependency 'TISwiftUtils', s.version.to_s diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec index c6a1fb90..35a86c34 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIWebView' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIYandexMapUtils/Sources/YandexMapManager.swift b/TIYandexMapUtils/Sources/YandexMapManager.swift index bddbf6fc..f9ae44e0 100644 --- a/TIYandexMapUtils/Sources/YandexMapManager.swift +++ b/TIYandexMapUtils/Sources/YandexMapManager.swift @@ -24,9 +24,10 @@ import TIMapUtils import YandexMapsMobile open class YandexMapManager: BaseMapManager, - YandexClusterPlacemarkManager, - YMKCameraUpdate> { + YandexPlacemarkManager, + YandexClusterPlacemarkManager, + YMKCameraUpdate, + YandexMapUISettings> { public init(map: YMKMapView, positionGetter: @escaping PositionGetter, diff --git a/TIYandexMapUtils/Sources/YandexMapUISettings.swift b/TIYandexMapUtils/Sources/YandexMapUISettings.swift new file mode 100644 index 00000000..17fc6d8b --- /dev/null +++ b/TIYandexMapUtils/Sources/YandexMapUISettings.swift @@ -0,0 +1,135 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIMapUtils +import YandexMapsMobile + +open class YandexMapUISettings: BaseMapUISettings { + open class UserLocationObjectListener: NSObject, YMKUserLocationObjectListener { + public var tintColor: UIColor { + didSet { + userLocationView?.accuracyCircle.fillColor = tintColor.withAlphaComponent(0.2) + } + } + + public var currentLocationIcon: UIImage { + didSet { + updateCurrentLocationIcon() + } + } + + private weak var userLocationView: YMKUserLocationView? + + public init(tintColor: UIColor, currentLocationIcon: UIImage) { + self.tintColor = tintColor + self.currentLocationIcon = currentLocationIcon + } + + // MARK: - YMKUserLocationObjectListener + + open func onObjectAdded(with view: YMKUserLocationView) { + self.userLocationView = view + + updateCurrentLocationIcon() + + view.accuracyCircle.fillColor = tintColor.withAlphaComponent(0.2) + } + + open func onObjectRemoved(with view: YMKUserLocationView) {} + + open func onObjectUpdated(with view: YMKUserLocationView, event: YMKObjectEvent) {} + + private func updateCurrentLocationIcon() { + userLocationView?.arrow.setIconWith(currentLocationIcon) + + let style = YMKIconStyle(anchor: CGPoint(x: 0.5, y: 0.5) as NSValue, + rotationType:YMKRotationType.rotate.rawValue as NSNumber, + zIndex: 0, + flat: true, + visible: true, + scale: 1, + tappableArea: nil) + + userLocationView?.pin.setIconWith(currentLocationIcon, style: style) + } + } + + public weak var userLocationLayer: YMKUserLocationLayer? + public weak var mapKit: YMKMapKit? + public var userLocationObjectListener: UserLocationObjectListener? + + public var tintColor: UIColor = .red { + didSet { + userLocationObjectListener?.tintColor = tintColor + } + } + + public var showHeadingArrow: Bool = true { + didSet { + userLocationObjectListener?.currentLocationIcon = currentLocationIcon(showHeadingArrow: showHeadingArrow) + } + } + + public init(mapKit: YMKMapKit = .sharedInstance()) { + self.mapKit = mapKit + } + + open override func apply(to mapView: YMKMapView) { + super.apply(to: mapView) + + if showUserLocation { + if userLocationLayer == nil { + userLocationLayer = mapKit?.createUserLocationLayer(with: mapView.mapWindow) + } + + let currentLocationIcon = currentLocationIcon(showHeadingArrow: showHeadingArrow) + + userLocationObjectListener = UserLocationObjectListener(tintColor: tintColor, + currentLocationIcon: currentLocationIcon) + } else { + userLocationObjectListener = nil + } + + userLocationLayer?.setVisibleWithOn(showUserLocation) + userLocationLayer?.setObjectListenerWith(userLocationObjectListener) + + let map = mapView.mapWindow.map + + map.isZoomGesturesEnabled = isZoomEnabled + map.isTiltGesturesEnabled = isTiltEnabled + map.isRotateGesturesEnabled = isRotationEnabled + } + + // MARK: - Subclass override + + open func currentLocationIcon(showHeadingArrow: Bool = true) -> UIImage { + let locationDrawingOperation = CurrentLocationDrawingOperation(mainColor: tintColor.cgColor, + borderColor: UIColor.white.cgColor, + showHeadingArrow: showHeadingArrow) + + let renderer = UIGraphicsImageRenderer(bounds: locationDrawingOperation.affectedArea()) + + return renderer.image { + locationDrawingOperation.apply(in: $0.cgContext) + } + } +} diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index 757a8b3c..10b8eca8 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.45.0' + s.version = '1.46.0' s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '12.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.static_framework = true s.user_target_xcconfig = { 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } diff --git a/docs/titextprocessing/titextprocessing.md b/docs/titextprocessing/titextprocessing.md index 00088acb..de1599e8 100644 --- a/docs/titextprocessing/titextprocessing.md +++ b/docs/titextprocessing/titextprocessing.md @@ -22,9 +22,9 @@ import Foundation import TITextProcessing -let textFormatter = TextFormatter(regex: "(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})") +let cardNumberTextFormatter = TextFormatter(regex: "(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})") -print(textFormatter.getRegexReplacement()) +print(cardNumberTextFormatter.getRegexReplacement()) /* Выведет в консоль: @@ -40,12 +40,7 @@ print(textFormatter.getRegexReplacement()) **Output**: `1234 5678 9012 3456` ```swift -import Foundation -import TITextProcessing - -let textFormatter = TextFormatter(regex: "(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})") - -print(textFormatter.getRegexPlaceholder()) +print(cardNumberTextFormatter.getRegexPlaceholder()) /* Выведет в консоль: @@ -63,12 +58,7 @@ print(textFormatter.getRegexPlaceholder()) > P.S. Учитываем, что `TextFormatter` был проинициализирован со слеюущим регулярным выражением: `(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})` ```swift -import Foundation -import TITextProcessing - -let textFormatter = TextFormatter(regex: "(\\d{4}) ?(\\d{4}) ?(\\d{4}) ?(\\d{4})") - -print(textFormatter.getFormattedText("2200111555550080")) +print(cardNumberTextFormatter.getFormattedText("2200111555550080")) /* Выведет в консоль: @@ -85,9 +75,6 @@ print(textFormatter.getFormattedText("2200111555550080")) Функция, преобразующий входящее регулярное выражение в структуру, содержащую шаблон подстановки и матрицу символов, например: ```swift -import Foundation -import TITextProcessing - let replaceGenerator: RegexReplaceGenerator = DefaultRegexReplaceGenerator() let item = replaceGenerator.generateReplacement(for: "(\\d{2})\\/?(\\d{2})") @@ -126,9 +113,6 @@ print(item.matrixOfSymbols) Функция, преобразующая входящую матрицу символов в текст-заполнитель, например: ```swift -import Foundation -import TITextProcessing - let matrix: [[Character]] = [ ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], @@ -151,9 +135,6 @@ print(placeholder) ## - Примеры использования: ```swift -import Foundation -import TITextProcessing - // MARK: - Форматирование даты let dateRegex = "(\\d{2})\\/?(\\d{2})" 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