Merge pull request #305 from TouchInstinct/feature/TIMapUtils

TIMapUtils
This commit is contained in:
Ivan Smolin 2022-04-27 11:29:36 +03:00 committed by GitHub
commit e31283cb67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1771 additions and 21 deletions

View File

@ -1,10 +1,14 @@
# Changelog
### 1.16.0
- **Add**: `TIMapUtils`, `TIAppleMapUtils`, `TIGoogleMapUtils` and `TIYandexMapUtils` modules for map items clustering and interacting with them.
### 1.15.0
- **Update**: Network services in TIMoyaNetworking now passes MoyaError in result of EnpointRequest execution.
- **Add**: `TINetworkingCache` module - caching results of EndpointRequests.
- **Important Note**: `TINetworkingCache` may require you to add `DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC=YES` flag to build settings of project target (see [probably related problem](https://forums.swift.org/t/adding-a-package-to-two-targets-in-one-projects-results-in-an-error/35007/18))
- **Important Note**: `TINetworkingCache` added via SPM may require you to add `DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC=YES` flag to build settings of project target (see [probably related problem](https://forums.swift.org/t/adding-a-package-to-two-targets-in-one-projects-results-in-an-error/35007/18))
### 1.14.3

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "LeadKit"
s.version = "1.15.0"
s.version = "1.16.0"
s.summary = "iOS framework with a bunch of tools for rapid development"
s.homepage = "https://github.com/TouchInstinct/LeadKit"
s.license = "Apache License, Version 2.0"

View File

@ -23,6 +23,11 @@ let package = Package(
.library(name: "TINetworking", targets: ["TINetworking"]),
.library(name: "TIMoyaNetworking", targets: ["TIMoyaNetworking"]),
.library(name: "TINetworkingCache", targets: ["TINetworkingCache"]),
// MARK: - Maps
.library(name: "TIMapUtils", targets: ["TIMapUtils"]),
.library(name: "TIAppleMapUtils", targets: ["TIAppleMapUtils"]),
// MARK: - Elements
.library(name: "OTPSwiftView", targets: ["OTPSwiftView"]),
@ -54,6 +59,11 @@ let package = Package(
.target(name: "TINetworking", dependencies: ["TISwiftUtils", "Alamofire"], path: "TINetworking/Sources"),
.target(name: "TIMoyaNetworking", dependencies: ["TINetworking", "TIFoundationUtils", "Moya"], path: "TIMoyaNetworking"),
.target(name: "TINetworkingCache", dependencies: ["TIFoundationUtils", "TINetworking", "Cache"], path: "TINetworkingCache/Sources"),
// MARK: - Maps
.target(name: "TIMapUtils", dependencies: [], path: "TIMapUtils/Sources"),
.target(name: "TIAppleMapUtils", dependencies: ["TIMapUtils"], path: "TIAppleMapUtils/Sources"),
// MARK: - Elements

View File

@ -0,0 +1,158 @@
//
// Copyright (c) 2022 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
import UIKit
open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView, [ApplePlacemarkManager<Model>], MKCoordinateRegion>, MKMapViewDelegate {
public weak var mapViewDelegate: MKMapViewDelegate?
private let mapDelegateSelectors = NSObject.instanceMethodSelectors(of: MKMapViewDelegate.self)
public init(placemarkManagers: [ApplePlacemarkManager<Model>],
mapViewDelegate: MKMapViewDelegate? = nil,
iconProvider: @escaping IconProviderClosure,
tapHandler: TapHandlerClosure?) {
self.mapViewDelegate = mapViewDelegate
super.init(dataModel: placemarkManagers,
iconProvider: iconProvider,
tapHandler: tapHandler)
}
public convenience init<IF: MarkerIconFactory>(placemarkManagers: [ApplePlacemarkManager<Model>],
mapViewDelegate: MKMapViewDelegate? = nil,
iconFactory: IF,
tapHandler: TapHandlerClosure?) where IF.Model == [Model] {
self.init(placemarkManagers: placemarkManagers,
mapViewDelegate: mapViewDelegate,
iconProvider: { iconFactory.markerIcon(for: $0.map { $0.dataModel }) },
tapHandler: tapHandler)
}
open func addMarkers(to map: MKMapView) {
map.delegate = self
map.addAnnotations(dataModel)
}
// MARK: - PlacemarkManager
override open func configure(placemark: MKAnnotationView) {
guard let clusterAnnotation = placemark.annotation as? MKClusterAnnotation,
let placemarkManagers = clusterAnnotation.memberAnnotations as? [ApplePlacemarkManager<Model>] else {
return
}
placemark.image = iconProvider(placemarkManagers)
}
// MARK: - MKMapViewDelegate
open func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(mapViewDelegate?.responds(to: #selector(mapView(_:viewFor:))) ?? false) else {
return mapViewDelegate?.mapView?(mapView, viewFor: annotation)
}
switch annotation {
case is MKClusterAnnotation:
let clusterAnnotationView = MKAnnotationView(annotation: annotation,
reuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)
configure(placemark: clusterAnnotationView)
return clusterAnnotationView
case let placemarkManager as ApplePlacemarkManager<Model>:
let defaultAnnotationView = MKAnnotationView(annotation: annotation,
reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
placemarkManager.configure(placemark: defaultAnnotationView)
return defaultAnnotationView
default:
return nil
}
}
open func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard !(mapViewDelegate?.responds(to: #selector(mapView(_:didSelect:))) ?? false) else {
mapViewDelegate?.mapView?(mapView, didSelect: view)
return
}
switch view.annotation {
case let clusterAnnotation as MKClusterAnnotation:
guard let placemarkManagers = clusterAnnotation.memberAnnotations as? [ApplePlacemarkManager<Model>] else {
return
}
_ = tapHandler?(placemarkManagers, .from(coordinates: placemarkManagers.map { $0.coordinate }))
case let placemarkManager as ApplePlacemarkManager<Model>:
_ = placemarkManager.tapHandler?(placemarkManager.dataModel, placemarkManager.coordinate)
default:
return
}
}
// MARK: - MKMapViewDelegate selectors forwarding
open override func responds(to aSelector: Selector) -> Bool {
let superResponds = super.responds(to: aSelector)
guard !superResponds else {
return superResponds
}
guard mapDelegateSelectors.contains(aSelector) else {
return superResponds
}
return mapViewDelegate?.responds(to: aSelector) ?? false
}
open override func forwardingTarget(for aSelector: Selector) -> Any? {
guard mapDelegateSelectors.contains(aSelector) else {
return nil
}
return mapViewDelegate
}
}
private extension MKCoordinateRegion {
static func from(coordinates: [CLLocationCoordinate2D]) -> MKCoordinateRegion {
guard !coordinates.isEmpty else {
return MKCoordinateRegion()
}
let bbox = CoordinateBounds.from(coordinates: coordinates)
let span = MKCoordinateSpan(latitudeDelta: bbox.northEast.latitude - bbox.southWest.latitude,
longitudeDelta: bbox.northEast.longitude - bbox.southWest.longitude)
let center = CLLocationCoordinate2D(latitude: bbox.northEast.latitude - (span.latitudeDelta / 2),
longitude: bbox.southWest.longitude + (span.longitudeDelta / 2))
return MKCoordinateRegion(center: center, span: span)
}
}

View File

@ -0,0 +1,66 @@
//
// Copyright (c) 2022 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 ApplePlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView, Model, CLLocationCoordinate2D>, MKAnnotation {
// MARK: - MKAnnotation
public let coordinate: CLLocationCoordinate2D
public var clusteringIdentifier: String?
public init(dataModel: Model,
coordinate: CLLocationCoordinate2D,
clusteringIdentifier: String?,
iconProvider: @escaping IconProviderClosure,
tapHandler: TapHandlerClosure?) {
self.coordinate = coordinate
self.clusteringIdentifier = clusteringIdentifier
super.init(dataModel: dataModel,
iconProvider: iconProvider,
tapHandler: tapHandler)
}
public convenience init<IF: MarkerIconFactory>(dataModel: Model,
coordinate: CLLocationCoordinate2D,
clusteringIdentifier: String?,
iconFactory: IF,
tapHandler: TapHandlerClosure?) where IF.Model == Model {
self.init(dataModel: dataModel,
coordinate: coordinate,
clusteringIdentifier: clusteringIdentifier,
iconProvider: { iconFactory.markerIcon(for: $0) },
tapHandler: tapHandler)
}
// MARK: - PlacemarkManager
override open func configure(placemark: MKAnnotationView) {
placemark.image = iconProvider(dataModel)
placemark.clusteringIdentifier = clusteringIdentifier
}
}

View File

@ -0,0 +1,42 @@
//
// Copyright (c) 2022 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 ObjectiveC
public extension NSObject {
static func instanceMethodSelectors(of protocol: Protocol) -> [Selector] {
var methodsCount: UInt32 = 0
let methodsList = protocol_copyMethodDescriptionList(`protocol`, false, true, &methodsCount)
defer {
methodsList?.deallocate()
}
var runtimeSelectors: [Selector?] = []
for offset in 0..<Int(methodsCount) {
runtimeSelectors.append(methodsList?.advanced(by: offset).pointee.name)
}
return runtimeSelectors.compactMap { $0 }
}
}

View File

@ -0,0 +1,16 @@
Pod::Spec.new do |s|
s.name = 'TIAppleMapUtils'
s.version = '1.16.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '11.0'
s.swift_versions = ['5.3']
s.source_files = s.name + '/Sources/**/*'
s.dependency 'TIMapUtils', s.version.to_s
end

View File

@ -32,9 +32,9 @@ public extension KeyedDecodingContainer {
using: try userInfo.dateFormatter(for: dateFormat))
}
func decodeDate<Format: DateFormat>(forKey key: Key,
userInfo: [CodingUserInfoKey: Any],
dateFormat: Format) throws -> [Date] {
func decodeDates<Format: DateFormat>(forKey key: Key,
userInfo: [CodingUserInfoKey: Any],
dateFormat: Format) throws -> [Date] {
let dateFormatter = try userInfo.dateFormatter(for: dateFormat)
@ -61,10 +61,10 @@ public extension KeyedDecodingContainer {
using: try userInfo.dateFormatter(for: dateFormat))
}
func decodeDate<Format: DateFormat>(forKey key: Key,
userInfo: [CodingUserInfoKey: Any],
dateFormat: Format,
required: Bool) throws -> [Date]? {
func decodeDates<Format: DateFormat>(forKey key: Key,
userInfo: [CodingUserInfoKey: Any],
dateFormat: Format,
required: Bool) throws -> [Date]? {
guard let stringDates = try decode([String]?.self, forKey: key, required: required) else {
return nil

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIFoundationUtils'
s.version = '1.15.0'
s.version = '1.16.0'
s.summary = 'Set of helpers for Foundation framework classes.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,133 @@
//
// Copyright (c) 2022 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 GoogleMapsUtils
import GoogleMaps
open class GoogleClusterPlacemarkManager<Model>: BasePlacemarkManager<GMSMarker, [GooglePlacemarkManager<Model>], GMSCoordinateBounds>,
GMUClusterRendererDelegate,
GMUClusterManagerDelegate,
GMUClusterIconGenerator {
public var mapDelegate: GMSMapViewDelegate? {
didSet {
clusterManager?.setMapDelegate(mapDelegate)
}
}
public private(set) var clusterManager: GMUClusterManager?
public init(placemarkManagers: [GooglePlacemarkManager<Model>],
iconProvider: @escaping IconProviderClosure,
tapHandler: TapHandlerClosure?) {
super.init(dataModel: placemarkManagers,
iconProvider: iconProvider,
tapHandler: tapHandler)
}
public convenience init<IF: MarkerIconFactory>(placemarkManagers: [GooglePlacemarkManager<Model>],
iconFactory: IF,
tapHandler: TapHandlerClosure?) where IF.Model == [Model] {
self.init(placemarkManagers: placemarkManagers,
iconProvider: { iconFactory.markerIcon(for: $0.map { $0.dataModel }) },
tapHandler: tapHandler)
}
open func clusterAlgorithm() -> GMUClusterAlgorithm {
GMUNonHierarchicalDistanceBasedAlgorithm()
}
open func clusterRenderer(for map: GMSMapView) -> GMUClusterRenderer {
let renderer = GMUDefaultClusterRenderer(mapView: map,
clusterIconGenerator: self)
renderer.delegate = self
return renderer
}
open func addMarkers(to map: GMSMapView) {
clusterManager = GMUClusterManager(map: map,
algorithm: clusterAlgorithm(),
renderer: clusterRenderer(for: map))
clusterManager?.setDelegate(self, mapDelegate: mapDelegate)
clusterManager?.add(dataModel)
clusterManager?.cluster()
}
// MARK: - GMUClusterRendererDelegate
open func renderer(_ renderer: GMUClusterRenderer, markerFor object: Any) -> GMSMarker? {
nil
}
open func renderer(_ renderer: GMUClusterRenderer, willRenderMarker marker: GMSMarker) {
switch marker.userData {
case let cluster as GMUCluster:
guard let placemarkManagers = cluster.items as? [GooglePlacemarkManager<Model>] else {
return
}
marker.icon = iconProvider(placemarkManagers)
case let clusterItem as GooglePlacemarkManager<Model>:
clusterItem.configure(placemark: marker)
default:
break
}
}
open func renderer(_ renderer: GMUClusterRenderer, didRenderMarker marker: GMSMarker) {
// nothing
}
// MARK: - GMUClusterManagerDelegate
open func clusterManager(_ clusterManager: GMUClusterManager, didTap cluster: GMUCluster) -> Bool {
guard let placemarkManagers = cluster.items as? [GooglePlacemarkManager<Model>] else {
return false
}
let bounds = placemarkManagers.reduce(GMSCoordinateBounds()) {
$0.includingCoordinate($1.position)
}
return tapHandler?(placemarkManagers, bounds) ?? false
}
open func clusterManager(_ clusterManager: GMUClusterManager, didTap clusterItem: GMUClusterItem) -> Bool {
guard let placemarkManager = clusterItem as? GooglePlacemarkManager<Model> else {
return false
}
return placemarkManager.tapHandler?(placemarkManager.dataModel, clusterItem.position) ?? false
}
// MARK: - GMUClusterIconGenerator
open func icon(forSize size: UInt) -> UIImage? {
nil // icon generation will be performed in GMUClusterRendererDelegate callback
}
}

View File

@ -0,0 +1,60 @@
//
// Copyright (c) 2022 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
import GoogleMapsUtils
open class GooglePlacemarkManager<Model>: BasePlacemarkManager<GMSMarker, Model, CLLocationCoordinate2D>, GMUClusterItem {
// MARK: - GMUClusterItem
public let position: CLLocationCoordinate2D
public init(dataModel: Model,
position: CLLocationCoordinate2D,
iconProvider: @escaping IconProviderClosure,
tapHandler: TapHandlerClosure?) {
self.position = position
super.init(dataModel: dataModel,
iconProvider: iconProvider,
tapHandler: tapHandler)
}
public convenience init<IF: MarkerIconFactory>(dataModel: Model,
position: CLLocationCoordinate2D,
iconFactory: IF,
tapHandler: TapHandlerClosure?) where IF.Model == Model {
self.init(dataModel: dataModel,
position: position,
iconProvider: { iconFactory.markerIcon(for: $0) },
tapHandler: tapHandler)
}
// MARK: - PlacemarkManager
override open func configure(placemark: GMSMarker) {
placemark.icon = iconProvider(dataModel)
}
}

View File

@ -0,0 +1,21 @@
Pod::Spec.new do |s|
s.name = 'TIGoogleMapUtils'
s.version = '1.16.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '12.0'
s.swift_versions = ['5.3']
s.source_files = s.name + '/Sources/**/*'
s.static_framework = true
s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' }
s.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' }
s.dependency 'TIMapUtils', s.version.to_s
s.dependency 'Google-Maps-iOS-Utils', '~> 4'
end

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIKeychainUtils'
s.version = '1.15.0'
s.version = '1.16.0'
s.summary = 'Set of helpers for Keychain classes.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,43 @@
//
// Copyright (c) 2022 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.CGGeometry
public typealias CGContextSize = (width: Int, height: Int)
public extension CGSize {
var ceiledContextSize: CGContextSize {
(width: Int(ceil(width)), height: Int(ceil(height)))
}
}
public extension CGPoint {
func horizontallyFlipped() -> Self {
CGPoint(x: x, y: -y)
}
}
public extension CGRect {
func offset(by point: CGPoint) -> Self {
offsetBy(dx: point.x, dy: point.y)
}
}

View File

@ -0,0 +1,68 @@
//
// Copyright (c) 2022 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.CGGeometry
public extension CGSize {
func resizeRect(forNewSize newSize: CGSize, resizeMode: ResizeMode) -> CGRect {
let horizontalRatio = newSize.width / width
let verticalRatio = newSize.height / height
let ratio: CGFloat
switch resizeMode {
case .scaleToFill:
ratio = 1
case .scaleAspectFill:
ratio = max(horizontalRatio, verticalRatio)
case .scaleAspectFit:
ratio = min(horizontalRatio, verticalRatio)
}
let newWidth = resizeMode == .scaleToFill ? newSize.width : width * ratio
let newHeight = resizeMode == .scaleToFill ? newSize.height : height * ratio
let originX: CGFloat
let originY: CGFloat
if newWidth > newSize.width {
originX = (newSize.width - newWidth) / 2
} else if newWidth < newSize.width {
originX = newSize.width / 2 - newWidth / 2
} else {
originX = 0
}
if newHeight > newSize.height {
originY = (newSize.height - newHeight) / 2
} else if newHeight < newSize.height {
originY = newSize.height / 2 - newHeight / 2
} else {
originY = 0
}
return CGRect(origin: CGPoint(x: originX, y: originY),
size: CGSize(width: newWidth, height: newHeight))
}
}

View File

@ -0,0 +1,25 @@
//
// Copyright (c) 2022 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 enum ResizeMode {
case scaleToFill, scaleAspectFit, scaleAspectFill
}

View File

@ -0,0 +1,83 @@
//
// Copyright (c) 2022 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 UIKit
public struct BorderDrawingOperation: DrawingOperation {
public var frameableContentSize: CGSize
public var border: CGFloat
public var color: CGColor
public var radius: CGFloat
public var exteriorBorder: Bool
public init(frameableContentSize: CGSize,
border: CGFloat,
color: CGColor,
radius: CGFloat,
exteriorBorder: Bool) {
self.frameableContentSize = frameableContentSize
self.border = border
self.color = color
self.radius = radius
self.exteriorBorder = exteriorBorder
}
// MARK: - 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
return CGRect(origin: .zero,
size: CGSize(width: width, height: height))
}
public func apply(in context: CGContext) {
let drawArea = affectedArea(in: context)
let ctxSize = drawArea.size.ceiledContextSize
let ctxRect = CGRect(origin: .zero, 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
let inset = ctxRect.insetBy(dx: border / 2 + widthDiff, dy: border / 2 + heightDiff)
context.setStrokeColor(color)
if radius != 0 {
context.setLineWidth(border)
let path = CGPath(roundedRect: inset,
cornerWidth: radius,
cornerHeight: radius,
transform: nil)
context.addPath(path)
context.strokePath()
} else {
context.stroke(inset, width: border)
}
}
}

View File

@ -0,0 +1,50 @@
//
// Copyright (c) 2022 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
import QuartzCore
public struct CALayerDrawingOperation: DrawingOperation {
public var layer: CALayer
public var offset: CGPoint
public init(layer: CALayer,
offset: CGPoint) {
self.layer = layer
self.offset = offset
}
// MARK: - DrawingOperation
public func affectedArea(in context: CGContext? = nil) -> CGRect {
CGRect(origin: offset, size: layer.bounds.size)
}
public func apply(in context: CGContext) {
let offsetTransform = CGAffineTransform(translationX: offset.x, y: offset.y)
context.concatenate(offsetTransform)
layer.render(in: context)
offsetTransform.concatenating(offsetTransform.inverted())
}
}

View File

@ -0,0 +1,28 @@
//
// Copyright (c) 2022 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 protocol DrawingOperation {
func affectedArea(in context: CGContext?) -> CGRect
func apply(in context: CGContext)
}

View File

@ -0,0 +1,57 @@
//
// Copyright (c) 2022 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 protocol OrientationAwareDrawingOperation: DrawingOperation {
var flipHorizontallyDuringDrawing: Bool { get set }
}
extension OrientationAwareDrawingOperation {
func apply(in context: CGContext, operation: (CGContext) -> Void) {
if flipHorizontallyDuringDrawing {
let flipVertical = CGAffineTransform(a: 1,
b: 0,
c: 0,
d: -1,
tx: 0,
ty: affectedArea(in: context).height)
context.concatenate(flipVertical)
operation(context)
context.concatenate(flipVertical.inverted())
} else {
operation(context)
}
}
func offsetForDrawing(_ offset: CGPoint) -> CGPoint {
flipHorizontallyDuringDrawing ? offset.horizontallyFlipped() : offset
}
func affectedAreaForDrawing(in context: CGContext?) -> CGRect {
var area = affectedArea(in: context)
area.origin = offsetForDrawing(area.origin)
return area
}
}

View File

@ -0,0 +1,78 @@
//
// Copyright (c) 2022 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 SolidFillDrawingOperation: DrawingOperation {
public enum Shape {
case rect(CGRect)
case ellipse(CGRect)
case path(CGPath)
}
public var color: CGColor
public var shape: Shape
public init(color: CGColor, shape: Shape) {
self.color = color
self.shape = shape
}
public init(color: CGColor, rect: CGRect) {
self.init(color: color, shape: .rect(rect))
}
public init(color: CGColor, ellipseRect: CGRect) {
self.init(color: color, shape: .ellipse(ellipseRect))
}
public init(color: CGColor, path: CGPath) {
self.init(color: color, shape: .path(path))
}
// MARK: - DrawingOperation
public func affectedArea(in context: CGContext? = nil) -> CGRect {
switch shape {
case let .rect(rect):
return rect
case let .ellipse(rect):
return rect
case let .path(path):
return path.boundingBox
}
}
public func apply(in context: CGContext) {
context.setFillColor(color)
switch shape {
case let .rect(rect):
context.fill(rect)
case let .ellipse(rect):
context.fillEllipse(in: rect)
case let .path(path):
context.addPath(path)
context.fillPath()
}
}
}

View File

@ -0,0 +1,74 @@
//
// Copyright (c) 2022 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
import Foundation.NSAttributedString
import CoreText
public struct TextDrawingOperation: OrientationAwareDrawingOperation {
public var text: String
public var font: CTFont
public var textColor: CGColor
public var flipHorizontallyDuringDrawing: Bool
public var desiredOffset: CGPoint
private var line: CTLine {
let textAttributes: [NSAttributedString.Key : Any] = [
.font: font,
.foregroundColor: textColor
]
let attributedString = NSAttributedString(string: text, attributes: textAttributes)
return CTLineCreateWithAttributedString(attributedString)
}
public init(text: String,
font: CTFont,
textColor: CGColor,
flipHorizontallyDuringDrawing: Bool = true,
desiredOffset: CGPoint = .zero) {
self.text = text
self.font = font
self.textColor = textColor
self.flipHorizontallyDuringDrawing = flipHorizontallyDuringDrawing
self.desiredOffset = desiredOffset
}
public func affectedArea(in context: CGContext? = nil) -> CGRect {
CGRect(origin: desiredOffset, size: CTLineGetImageBounds(line, context).size)
}
public func apply(in context: CGContext) {
apply(in: context) {
let originForDrawing = offsetForDrawing(CTLineGetImageBounds(line, context).origin)
let desiredOffsetForDrawing = offsetForDrawing(desiredOffset)
let textPosition = CGPoint(x: desiredOffsetForDrawing.x - originForDrawing.x,
y: desiredOffsetForDrawing.y - originForDrawing.y)
$0.textPosition = textPosition
CTLineDraw(line, $0)
}
}
}

View File

@ -0,0 +1,73 @@
//
// Copyright (c) 2022 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 TransformDrawingOperation: OrientationAwareDrawingOperation {
public var image: CGImage
public var imageSize: CGSize
public var maxNewSize: CGSize
public var resizeMode: ResizeMode
public var offset: CGPoint
public var flipHorizontallyDuringDrawing: Bool
public var cropToImageBounds: Bool
public var interpolationQuality: CGInterpolationQuality
private var resizedRect: CGRect {
imageSize.resizeRect(forNewSize: maxNewSize, resizeMode: resizeMode)
}
public init(image: CGImage,
imageSize: CGSize,
maxNewSize: CGSize,
resizeMode: ResizeMode = .scaleAspectFit,
offset: CGPoint = .zero,
flipHorizontallyDuringDrawing: Bool = true,
cropToImageBounds: Bool = false,
interpolationQuality: CGInterpolationQuality = .default) {
self.image = image
self.imageSize = imageSize
self.maxNewSize = maxNewSize
self.resizeMode = resizeMode
self.offset = offset
self.flipHorizontallyDuringDrawing = flipHorizontallyDuringDrawing
self.cropToImageBounds = cropToImageBounds
self.interpolationQuality = interpolationQuality
}
// MARK: - DrawingOperation
public func affectedArea(in context: CGContext? = nil) -> CGRect {
cropToImageBounds
? CGRect(origin: offset, size: resizedRect.size)
: resizedRect.offset(by: offset)
}
public func apply(in context: CGContext) {
apply(in: context) {
$0.interpolationQuality = interpolationQuality
$0.draw(image, in: affectedAreaForDrawing(in: $0))
}
}
}

View File

@ -0,0 +1,51 @@
//
// Copyright (c) 2022 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
public struct CoordinateBounds {
public let southWest: CLLocationCoordinate2D
public let northEast: CLLocationCoordinate2D
public init(southWest: CLLocationCoordinate2D, northEast: CLLocationCoordinate2D) {
self.southWest = southWest
self.northEast = northEast
}
}
public extension CoordinateBounds {
static func from<C: Collection>(coordinates: C) -> CoordinateBounds where C.Element == CLLocationCoordinate2D {
guard let first = coordinates.first else {
return .init(southWest: CLLocationCoordinate2D(),
northEast: CLLocationCoordinate2D())
}
let initialBox = CoordinateBounds(southWest: first, northEast: first)
return coordinates.dropFirst().reduce(initialBox) {
CoordinateBounds(southWest: CLLocationCoordinate2D(latitude: min($0.southWest.latitude, $1.latitude),
longitude: min($0.southWest.longitude, $1.longitude)),
northEast: CLLocationCoordinate2D(latitude: max($0.northEast.latitude, $1.latitude),
longitude: max($0.northEast.longitude, $1.longitude)))
}
}
}

View File

@ -0,0 +1,52 @@
//
// Copyright (c) 2022 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 UIKit.UIImage
open class DefaultCachableMarkerIconFactory<M, K: AnyObject>: DefaultMarkerIconFactory<M> {
public typealias CacheKeyProvider = (M) -> K
public let cache = NSCache<K, UIImage>()
private let cacheKeyProvider: CacheKeyProvider
public init(createIconClosure: @escaping CreateIconClosure,
cacheKeyProvider: @escaping CacheKeyProvider) {
self.cacheKeyProvider = cacheKeyProvider
super.init(createIconClosure: createIconClosure)
}
open override func markerIcon(for model: M) -> UIImage {
let cacheKey = cacheKeyProvider(model)
guard let cachedIcon = cache.object(forKey: cacheKey) else {
let icon = super.markerIcon(for: model)
cache.setObject(icon, forKey: cacheKey)
return icon
}
return cachedIcon
}
}

View File

@ -0,0 +1,173 @@
//
// Copyright (c) 2022 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 UIKit
open class DefaultClusterIconRenderer {
public struct TextAttributes {
public var font: UIFont
public var color: UIColor
public init(font: UIFont, color: UIColor) {
self.font = font
self.color = color
}
}
public enum Background {
case color(UIColor)
case image(UIImage)
}
public struct Border {
public var strokeSize: CGFloat
public var color: UIColor
public init(strokeSize: CGFloat, color: UIColor) {
self.strokeSize = strokeSize
self.color = color
}
}
public var screenScale: CGFloat
public var textAttributes: TextAttributes
public var marginToText: CGFloat
public var background: Background
public var border: Border
private var borderWidth: CGFloat {
border.strokeSize
}
public init(screenScale: CGFloat,
textAttributes: TextAttributes = .init(font: .systemFont(ofSize: 48),
color: .black),
marginToText: CGFloat = 8,
background: Background = .color(.orange),
border: Border = .init(strokeSize: 4, color: .white)) {
self.screenScale = screenScale
self.textAttributes = textAttributes
self.marginToText = marginToText
self.background = background
self.border = border
}
open func format(clusterSize: Int) -> String {
String(clusterSize)
}
open func textDrawingOperation(for text: String) -> TextDrawingOperation {
let ctFont = CTFontCreateWithFontDescriptorAndOptions(textAttributes.font.fontDescriptor,
textAttributes.font.pointSize,
nil,
[])
return TextDrawingOperation(text: text,
font: ctFont,
textColor: textAttributes.color.cgColor)
}
open func backgroundDrawingOperation(iconSize: CGSize,
iconSizeWithBorder: CGSize,
cornerRadius: CGFloat) -> DrawingOperation? {
switch background {
case let .color(color):
let path = CGPath(roundedRect: CGRect(origin: CGPoint(x: borderWidth, y: borderWidth),
size: iconSize),
cornerWidth: cornerRadius,
cornerHeight: cornerRadius,
transform: nil)
return SolidFillDrawingOperation(color: color.cgColor,
path: path)
case let .image(image):
guard let cgImage = image.cgImage else {
return nil
}
return TransformDrawingOperation(image: cgImage,
imageSize: image.size,
maxNewSize: iconSize,
flipHorizontallyDuringDrawing: true)
}
}
open func borderDrawingOperation(iconSize: CGSize,
cornerRadius: CGFloat) -> DrawingOperation {
BorderDrawingOperation(frameableContentSize: iconSize,
border: borderWidth,
color: border.color.cgColor,
radius: cornerRadius,
exteriorBorder: true)
}
open func execute(drawingOperations: [DrawingOperation], inContextWithSize size: CGSize) -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.opaque = false
format.scale = screenScale
let renderer = UIGraphicsImageRenderer(size: size, format: format)
return renderer.image {
for operation in drawingOperations {
operation.apply(in: $0.cgContext)
}
}
}
open func renderCluster(of size: Int) -> UIImage {
let text = format(clusterSize: size)
var textDrawingOperation = textDrawingOperation(for: text)
let textSize = textDrawingOperation.affectedArea().size
let textRadius = sqrt(textSize.height * textSize.height + textSize.width * textSize.width) / 2
let internalRadius = textRadius + marginToText
let iconSize = CGSize(width: internalRadius * 2, height: internalRadius * 2)
let iconSizeWithBorder = CGSize(width: iconSize.width + borderWidth * 2,
height: iconSize.height + borderWidth * 2)
let radius = CGFloat(min(iconSizeWithBorder.width, iconSizeWithBorder.height) / 2)
textDrawingOperation.desiredOffset = CGPoint(x: (iconSizeWithBorder.width - textSize.width) / 2,
y: (iconSizeWithBorder.height - textSize.height) / 2)
let backgroundDrawingOperation = backgroundDrawingOperation(iconSize: iconSize,
iconSizeWithBorder: iconSizeWithBorder,
cornerRadius: radius)
let borderDrawindOperation = borderDrawingOperation(iconSize: iconSize,
cornerRadius: radius)
let operations = [backgroundDrawingOperation,
textDrawingOperation,
borderDrawindOperation]
.compactMap { $0 }
return execute(drawingOperations: operations,
inContextWithSize: iconSizeWithBorder)
}
}

View File

@ -0,0 +1,41 @@
//
// Copyright (c) 2022 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 UIKit.UIImage
open class DefaultMarkerIconFactory<M>: MarkerIconFactory {
public typealias CreateIconClosure = (M) -> UIImage
private let createIconClosure: CreateIconClosure
public init(createIconClosure: @escaping CreateIconClosure) {
self.createIconClosure = createIconClosure
}
open func markerIcon(for model: M) -> UIImage {
postprocess(icon: createIconClosure(model))
}
open func postprocess(icon: UIImage) -> UIImage {
icon
}
}

View File

@ -0,0 +1,29 @@
//
// Copyright (c) 2022 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 UIKit.UIImage
public protocol MarkerIconFactory {
associatedtype Model
func markerIcon(for model: Model) -> UIImage
}

View File

@ -0,0 +1,57 @@
//
// Copyright (c) 2022 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 UIKit
open class BasePlacemarkManager<Placemark, Model, Position>: NSObject, PlacemarkManager {
public typealias TapHandlerClosure = (Model, Position) -> Bool
public typealias IconProviderClosure = (Model) -> UIImage
public var tapHandler: TapHandlerClosure?
public var iconProvider: IconProviderClosure
public let dataModel: Model
public init(dataModel: Model,
iconProvider: @escaping IconProviderClosure,
tapHandler: TapHandlerClosure?) {
self.dataModel = dataModel
self.iconProvider = iconProvider
self.tapHandler = tapHandler
}
public convenience init<IF: MarkerIconFactory>(dataModel: Model,
iconFactory: IF,
tapHandler: TapHandlerClosure?) where IF.Model == Model {
self.init(dataModel: dataModel,
iconProvider: { iconFactory.markerIcon(for: $0) },
tapHandler: tapHandler)
}
// MARK: - PlacemarkManager
open func configure(placemark: Placemark) {
// override in subclass
}
}

View File

@ -0,0 +1,27 @@
//
// Copyright (c) 2022 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 PlacemarkManager {
associatedtype Placemark
func configure(placemark: Placemark)
}

View File

@ -0,0 +1,15 @@
Pod::Spec.new do |s|
s.name = 'TIMapUtils'
s.version = '1.16.0'
s.summary = 'Set of helpers for map objects clustering and interacting.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '10.0'
s.swift_versions = ['5.3']
s.source_files = s.name + '/Sources/**/*'
end

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMoyaNetworking'
s.version = '1.15.0'
s.version = '1.16.0'
s.summary = 'Moya + Swagger network service.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworking'
s.version = '1.15.0'
s.version = '1.16.0'
s.summary = 'Swagger-frendly networking layer helpers.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworkingCache'
s.version = '1.15.0'
s.version = '1.16.0'
s.summary = 'Caching results of EndpointRequests.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,17 @@
Pod::Spec.new do |s|
s.name = 'TIPagination'
s.version = '1.16.0'
s.summary = 'Generic pagination component.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '10.0'
s.swift_versions = ['5.3']
s.source_files = s.name + '/Sources/**/*'
s.dependency 'TISwiftUtils', s.version.to_s
s.dependency 'Cursors', "~> 0.6.0"
end

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TISwiftUtils'
s.version = '1.15.0'
s.version = '1.16.0'
s.summary = 'Bunch of useful helpers for Swift development.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITableKitUtils'
s.version = '1.15.0'
s.version = '1.16.0'
s.summary = 'Set of helpers for TableKit classes.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITransitions'
s.version = '1.15.0'
s.version = '1.16.0'
s.summary = 'Set of custom transitions to present controller. '
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIElements'
s.version = '1.15.0'
s.version = '1.16.0'
s.summary = 'Bunch of useful protocols and views.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIKitCore'
s.version = '1.15.0'
s.version = '1.16.0'
s.summary = 'Core UI elements: protocols, views and helpers.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,109 @@
//
// Copyright (c) 2022 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
import UIKit
open class YandexClusterPlacemarkManager<Model>: BasePlacemarkManager<YMKCluster, [YandexPlacemarkManager<Model>], YMKBoundingBox>, YMKClusterListener, YMKClusterTapListener {
public var placemarksMapping: Zip2Sequence<[YMKPlacemarkMapObject], [YandexPlacemarkManager<Model>]>?
public init(placemarkManagers: [YandexPlacemarkManager<Model>],
iconProvider: @escaping IconProviderClosure,
tapHandler: TapHandlerClosure?) {
super.init(dataModel: placemarkManagers,
iconProvider: iconProvider,
tapHandler: tapHandler)
}
public convenience init<IF: MarkerIconFactory>(placemarkManagers: [YandexPlacemarkManager<Model>],
iconFactory: IF,
tapHandler: TapHandlerClosure?) where IF.Model == [Model] {
self.init(placemarkManagers: placemarkManagers,
iconProvider: { iconFactory.markerIcon(for: $0.map { $0.dataModel }) },
tapHandler: tapHandler)
}
open func addMarkers(to map: YMKMap, clusterRadius: Double = 60, minZoom: UInt = 15) {
let clusterizedPlacemarkCollection = map.mapObjects.addClusterizedPlacemarkCollection(with: self)
let emptyPlacemarks = clusterizedPlacemarkCollection.addEmptyPlacemarks(with: dataModel.map { $0.position })
self.placemarksMapping = zip(emptyPlacemarks, dataModel)
placemarksMapping?.forEach { (placemark, manager) in
manager.configure(placemark: placemark)
}
clusterizedPlacemarkCollection.clusterPlacemarks(withClusterRadius: clusterRadius,
minZoom: minZoom)
}
// MARK: - YMKClusterListener
open func onClusterAdded(with cluster: YMKCluster) {
configure(placemark: cluster)
}
// MARK: - YMKClusterTapListener
open func onClusterTap(with cluster: YMKCluster) -> Bool {
guard let tapHandler = tapHandler else {
return false
}
return tapHandler(managers(in: cluster), .from(coordinates: managers(in: cluster).map { $0.position }))
}
open func managers(in cluster: YMKCluster) -> [YandexPlacemarkManager<Model>] {
cluster.placemarks.compactMap { placemark in
placemarksMapping?.first { $0.0 == placemark }?.1
}
}
// MARK: - PlacemarkManager
open override func configure(placemark: YMKCluster) {
placemark.addClusterTapListener(with: self)
placemark.appearance.setIconWith(iconProvider(managers(in: placemark)))
}
}
private extension YMKBoundingBox {
static func from(coordinates: [YMKPoint]) -> YMKBoundingBox {
guard let first = coordinates.first else {
return YMKBoundingBox()
}
let initialBox = YMKBoundingBox(southWest: first, northEast: first)
return coordinates.dropFirst().reduce(initialBox) {
YMKBoundingBox(southWest: YMKPoint(latitude: min($0.southWest.latitude, $1.latitude),
longitude: min($0.southWest.longitude, $1.longitude)),
northEast: YMKPoint(latitude: max($0.northEast.latitude, $1.latitude),
longitude: max($0.northEast.longitude, $1.longitude)))
}
}
}

View File

@ -0,0 +1,64 @@
//
// Copyright (c) 2022 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 YandexPlacemarkManager<Model>: BasePlacemarkManager<YMKPlacemarkMapObject, Model, YMKPoint>, YMKMapObjectTapListener {
public let position: YMKPoint
public init(dataModel: Model,
position: YMKPoint,
iconProvider: @escaping IconProviderClosure,
tapHandler: TapHandlerClosure?) {
self.position = position
super.init(dataModel: dataModel,
iconProvider: iconProvider,
tapHandler: tapHandler)
}
public convenience init<IF: MarkerIconFactory>(dataModel: Model,
position: YMKPoint,
iconFactory: IF,
tapHandler: TapHandlerClosure?) where IF.Model == Model {
self.init(dataModel: dataModel,
position: position,
iconProvider: { iconFactory.markerIcon(for: $0) },
tapHandler: tapHandler)
}
// MARK: - YMKMapObjectTapListener
public func onMapObjectTap(with mapObject: YMKMapObject, point: YMKPoint) -> Bool {
tapHandler?(dataModel, point) ?? false
}
// MARK: - PlacemarkManager
override open func configure(placemark: YMKPlacemarkMapObject) {
placemark.addTapListener(with: self)
placemark.setIconWith(iconProvider(dataModel))
}
}

View File

@ -0,0 +1,21 @@
Pod::Spec.new do |s|
s.name = 'TIYandexMapUtils'
s.version = '1.16.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '10.0'
s.swift_versions = ['5.3']
s.source_files = s.name + '/Sources/**/*'
s.static_framework = true
s.user_target_xcconfig = { 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
s.pod_target_xcconfig = { 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
s.dependency 'TIMapUtils', s.version.to_s
s.dependency 'YandexMapsMobile', '4.0.0-lite'
end

View File

@ -6,14 +6,19 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$DIR"
ORDERED_PODSPECS="../TISwiftUtils/TISwiftUtils.podspec
../TIPagination/TIPagination.podspec
../TIFoundationUtils/TIFoundationUtils.podspec
../TIKeychainUtils/TIKeychainUtils.podspec
../TIUIKitCore/TIUIKitCore.podspec
../TIUIElements/TIUIElements.podspec
../TITableKitUtils/TITableKitUtils.podspec
../TINetworking/TINetworking.podspec
../TINetworking/TINetworkingCache.podspec
../TIMoyaNetworking/TIMoyaNetworking.podspec"
../TINetworkingCache/TINetworkingCache.podspec
../TIMoyaNetworking/TIMoyaNetworking.podspec
../TIMapUtils/TIMapUtils.podspec
../TIAppleMapUtils/TIAppleMapUtils.podspec
../TIGoogleMapUtils/TIGoogleMapUtils.podspec
../TIYandexMapUtils/TIYandexMapUtils.podspec"
for podspec_path in ${ORDERED_PODSPECS}; do
bundle exec pod repo push git@github.com:TouchInstinct/Podspecs ${podspec_path} --allow-warnings