feat: Placemark appearance updating added

This commit is contained in:
Vladimir Makarov 2023-03-03 08:50:16 +01:00
parent 278e175f3a
commit 144ea7b703
16 changed files with 251 additions and 36 deletions

View File

@ -24,7 +24,9 @@ import TIMapUtils
import MapKit
import UIKit
open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView, [ApplePlacemarkManager<Model>], MKMapRect>, MKMapViewDelegate {
open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView, [ApplePlacemarkManager<Model>], MKMapRect>,
MKMapViewDelegate {
public weak var mapViewDelegate: MKMapViewDelegate?
private let mapDelegateSelectors = NSObject.instanceMethodSelectors(of: MKMapViewDelegate.self)
@ -49,16 +51,26 @@ open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotatio
open func removeMarkers(from map: MKMapView) {
map.removeAnnotations(dataModel)
}
/// Manual selecting of a placemark with an incoming point coordinates
open func selectMarker(with point: CLLocationCoordinate2D) {
dataModel.filter { $0.coordinate == point }.forEach { $0.state = .selected }
}
/// Manual state resetting of all placemarks with currently selected state
open func resetMarkersState() {
dataModel.filter { $0.state == .selected }.forEach { $0.state = .default }
}
// MARK: - PlacemarkManager
override open func configure(placemark: MKAnnotationView) {
guard let clusterAnnotation = placemark.annotation as? MKClusterAnnotation,
let placemarkManagers = clusterAnnotation.memberAnnotations as? [ApplePlacemarkManager<Model>] else {
return
}
return
}
placemark.image = iconFactory?.markerIcon(for: placemarkManagers)
placemark.image = iconFactory?.markerIcon(for: placemarkManagers, state: .default)
}
// MARK: - MKMapViewDelegate
@ -79,6 +91,7 @@ open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotatio
configure(placemark: defaultAnnotationView)
return defaultAnnotationView
case let placemarkManager as ApplePlacemarkManager<Model>:
let defaultAnnotationView = placemarkManager.iconFactory != nil
? MKAnnotationView(annotation: annotation,
@ -89,6 +102,7 @@ open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotatio
placemarkManager.configure(placemark: defaultAnnotationView)
return defaultAnnotationView
default:
return nil
}
@ -107,8 +121,29 @@ open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotatio
}
_ = tapHandler?(placemarkManagers, .from(coordinates: placemarkManagers.map { $0.coordinate }))
case let placemarkManager as ApplePlacemarkManager<Model>:
_ = placemarkManager.tapHandler?(placemarkManager.dataModel, placemarkManager.coordinate)
let isTapHandled = placemarkManager.tapHandler?(placemarkManager.dataModel, placemarkManager.coordinate) ?? false
if isTapHandled {
placemarkManager.state = .selected
}
default:
return
}
}
open func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
guard !(mapViewDelegate?.responds(to: #selector(mapView(_:didDeselect:))) ?? false) else {
mapViewDelegate?.mapView?(mapView, didDeselect: view)
return
}
switch view.annotation {
case let placemarkManager as ApplePlacemarkManager<Model>:
placemarkManager.state = .default
default:
return
}

View File

@ -44,7 +44,8 @@ open class AppleMapManager<DataModel>: BaseMapManager<MKMapView,
return nil
}
return ApplePlacemarkManager(dataModel: $0,
return ApplePlacemarkManager(map: map,
dataModel: $0,
position: position,
clusteringIdentifier: clusteringIdentifierGetter($0),
iconFactory: iconFactory?.asAnyMarkerIconFactory(),

View File

@ -23,19 +23,50 @@
import TIMapUtils
import MapKit
open class ApplePlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView, Model, CLLocationCoordinate2D>, MKAnnotation {
open class ApplePlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView, Model, CLLocationCoordinate2D>,
MKAnnotation {
// MARK: - MKAnnotation
/// A map where all placemarks are placed
public let map: MKMapView
/// Point (coordinates) itself of the current placemark manager
public let coordinate: CLLocationCoordinate2D
/// Identifier required for correct cluster placement
public var clusteringIdentifier: String?
/// Placemark itself of the current placemark manager
public private(set) var placemark: MKAnnotationView?
/// The current state of a manager's placemark
public var state: MarkerState = .default {
didSet {
guard let placemark = placemark else {
return
}
/*
Although the icon is being updated, it is necessary to manually deselect
the annotation of the current placemark if it is currently selected.
*/
if state == .default, let annotation = placemark.annotation {
map.deselectAnnotation(annotation, animated: true)
}
placemark.image = iconFactory?.markerIcon(for: dataModel, state: state)
}
}
public init(dataModel: Model,
public init(map: MKMapView,
dataModel: Model,
position: CLLocationCoordinate2D,
clusteringIdentifier: String?,
iconFactory: AnyMarkerIconFactory<DataModel>?,
tapHandler: TapHandlerClosure?) {
self.map = map
self.coordinate = position
self.clusteringIdentifier = clusteringIdentifier
@ -47,7 +78,9 @@ open class ApplePlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView,
// MARK: - PlacemarkManager
override open func configure(placemark: MKAnnotationView) {
placemark.image = iconFactory?.markerIcon(for: dataModel)
placemark.clusteringIdentifier = clusteringIdentifier
// Setting initial values in cluster configuration and each placemark respectively
self.placemark = placemark
self.placemark?.clusteringIdentifier = clusteringIdentifier
self.state = .default
}
}

View File

@ -77,6 +77,16 @@ open class GoogleClusterPlacemarkManager<Model>: BasePlacemarkManager<GMSMarker,
open func removeMarkers() {
clusterManager?.clearItems()
}
/// Manual selecting of a placemark with an incoming point coordinates
open func selectMarker(with point: CLLocationCoordinate2D) {
dataModel.filter { $0.position == point }.forEach { $0.state = .selected }
}
/// Manual state resetting of all placemarks with currently selected state
open func resetMarkersState() {
dataModel.filter { $0.state == .selected }.forEach { $0.state = .default }
}
// MARK: - GMUClusterRendererDelegate
@ -91,7 +101,7 @@ open class GoogleClusterPlacemarkManager<Model>: BasePlacemarkManager<GMSMarker,
return
}
marker.icon = iconFactory?.markerIcon(for: placemarkManagers)
marker.icon = iconFactory?.markerIcon(for: placemarkManagers, state: .default)
?? defaultClusterIconGenerator.icon(forSize: UInt(placemarkManagers.count))
case let clusterItem as GooglePlacemarkManager<Model>:
clusterItem.configure(placemark: marker)

View File

@ -24,10 +24,27 @@ import TIMapUtils
import GoogleMaps
import GoogleMapsUtils
open class GooglePlacemarkManager<Model>: BasePlacemarkManager<GMSMarker, Model, CLLocationCoordinate2D>, GMUClusterItem {
open class GooglePlacemarkManager<Model>: BasePlacemarkManager<GMSMarker, Model, CLLocationCoordinate2D>,
GMUClusterItem {
// MARK: - GMUClusterItem
/// Point (coordinates) itself of the current placemark manager
public let position: CLLocationCoordinate2D
/// Placemark itself of the current placemark manager
public private(set) var placemark: GMSMarker?
/// The current state of a manager's placemark
public var state: MarkerState = .default {
didSet {
guard let placemark = placemark else {
return
}
placemark.icon = iconFactory?.markerIcon(for: dataModel, state: state)
}
}
public init(dataModel: Model,
position: CLLocationCoordinate2D,
@ -44,6 +61,14 @@ open class GooglePlacemarkManager<Model>: BasePlacemarkManager<GMSMarker, Model,
// MARK: - PlacemarkManager
override open func configure(placemark: GMSMarker) {
placemark.icon = iconFactory?.markerIcon(for: dataModel)
// Setting initial values in cluster configuration and each placemark respectively
self.placemark = placemark
/*
Note: it is required not to just resetting a state but setting a value depending
on a previous state due to Google Maps' map re-rendering on zoom to save
an appearance of the pin as it was previously
*/
self.state = state == .default ? .default : .selected
}
}

View File

@ -0,0 +1,29 @@
//
// 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
extension CLLocationCoordinate2D: Equatable {
public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
}

View File

@ -23,20 +23,20 @@
import UIKit.UIImage
public final class AnyMarkerIconFactory<Model>: MarkerIconFactory {
public typealias IconProviderClosure = (Model) -> UIImage
public typealias IconProviderClosure = (Model, MarkerState) -> UIImage
public var iconProviderClosure: IconProviderClosure
public init<IF: MarkerIconFactory>(iconFactory: IF) where IF.Model == Model {
self.iconProviderClosure = { iconFactory.markerIcon(for: $0) }
self.iconProviderClosure = { iconFactory.markerIcon(for: $0, state: $1) }
}
public init<IF: MarkerIconFactory, T>(iconFactory: IF, transform: @escaping (Model) -> T) where IF.Model == T {
self.iconProviderClosure = { iconFactory.markerIcon(for: transform($0)) }
self.iconProviderClosure = { iconFactory.markerIcon(for: transform($0), state: $1) }
}
public func markerIcon(for model: Model) -> UIImage {
iconProviderClosure(model)
public func markerIcon(for model: Model, state: MarkerState) -> UIImage {
iconProviderClosure(model, state)
}
}

View File

@ -37,11 +37,11 @@ open class DefaultCachableMarkerIconFactory<M, K: AnyObject>: DefaultMarkerIconF
super.init(createIconClosure: createIconClosure)
}
open override func markerIcon(for model: M) -> UIImage {
open override func markerIcon(for model: M, state: MarkerState) -> UIImage {
let cacheKey = cacheKeyProvider(model)
guard let cachedIcon = cache.object(forKey: cacheKey) else {
let icon = super.markerIcon(for: model)
let icon = super.markerIcon(for: model, state: .default)
cache.setObject(icon, forKey: cacheKey)
return icon

View File

@ -32,10 +32,10 @@ public final class DefaultClusterMarkerIconFactory<Model>: DefaultCachableMarker
self.beforeRenderCallback = beforeRenderCallback
self.clusterIconRenderer = DefaultClusterIconRenderer()
super.init { [clusterIconRenderer] in
beforeRenderCallback?($0, clusterIconRenderer)
super.init { [clusterIconRenderer] models, _ in
beforeRenderCallback?(models, clusterIconRenderer)
return clusterIconRenderer.renderCluster(of: $0.count)
return clusterIconRenderer.renderCluster(of: models.count)
} cacheKeyProvider: {
String($0.count) as NSString
}

View File

@ -23,7 +23,7 @@
import UIKit.UIImage
open class DefaultMarkerIconFactory<M>: MarkerIconFactory {
public typealias CreateIconClosure = (M) -> UIImage
public typealias CreateIconClosure = (M, MarkerState) -> UIImage
private let createIconClosure: CreateIconClosure
@ -31,8 +31,8 @@ open class DefaultMarkerIconFactory<M>: MarkerIconFactory {
self.createIconClosure = createIconClosure
}
open func markerIcon(for model: M) -> UIImage {
postprocess(icon: createIconClosure(model))
open func markerIcon(for model: M, state: MarkerState) -> UIImage {
postprocess(icon: createIconClosure(model, state))
}
open func postprocess(icon: UIImage) -> UIImage {

View File

@ -25,5 +25,5 @@ import UIKit.UIImage
public protocol MarkerIconFactory {
associatedtype Model
func markerIcon(for model: Model) -> UIImage
func markerIcon(for model: Model, state: MarkerState) -> UIImage
}

View File

@ -0,0 +1,31 @@
//
// 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.
//
/// Available marker states on any map
public enum MarkerState {
/// A state where a map point is selected and a marker is highlighted
case selected
/// A default state where a map point is shown on a map
case `default`
}

View File

@ -29,7 +29,7 @@ public final class StaticImageIconFactory<Model>: MarkerIconFactory {
self.image = image
}
public func markerIcon(for model: Model) -> UIImage {
public func markerIcon(for model: Model, state: MarkerState) -> UIImage {
image
}
}

View File

@ -27,5 +27,15 @@ public protocol PlacemarkManager {
typealias TapHandlerClosure = (DataModel, Position) -> Bool
var dataModel: DataModel { get }
///
/// Validates whether the current tap could be handled or not
///
/// - Parameters:
/// - DataModel: A data model of the current placemark manager
/// - Position: A position of the current placemark
///
/// - Returns: A `Bool` value of the handling desicion
///
var tapHandler: TapHandlerClosure? { get set }
}

View File

@ -25,7 +25,9 @@ import YandexMapsMobile
import UIKit
import CoreLocation
open class YandexClusterPlacemarkManager<Model>: BasePlacemarkManager<YMKCluster, [YandexPlacemarkManager<Model>], [YMKPoint]>, YMKClusterListener, YMKClusterTapListener {
open class YandexClusterPlacemarkManager<Model>: BasePlacemarkManager<YMKCluster, [YandexPlacemarkManager<Model>], [YMKPoint]>,
YMKClusterListener,
YMKClusterTapListener {
private var placemarkCollection: YMKClusterizedPlacemarkCollection?
@ -63,6 +65,16 @@ open class YandexClusterPlacemarkManager<Model>: BasePlacemarkManager<YMKCluster
placemarkCollection?.clear()
}
/// Manual selecting of a placemark with an incoming point coordinates
open func selectMarker(with point: YMKPoint) {
dataModel.filter { $0.position == point }.forEach { $0.state = .selected }
}
/// Manual state resetting of all placemarks with currently selected state
open func resetMarkersState() {
dataModel.filter { $0.state == .selected }.forEach { $0.state = .default }
}
// MARK: - YMKClusterListener
open func onClusterAdded(with cluster: YMKCluster) {
@ -92,7 +104,7 @@ open class YandexClusterPlacemarkManager<Model>: BasePlacemarkManager<YMKCluster
open override func configure(placemark: YMKCluster) {
placemark.addClusterTapListener(with: self)
if let customIcon = iconFactory?.markerIcon(for: managers(in: placemark)) {
if let customIcon = iconFactory?.markerIcon(for: managers(in: placemark), state: .default) {
placemark.appearance.setIconWith(customIcon)
}
}

View File

@ -23,8 +23,27 @@
import TIMapUtils
import YandexMapsMobile
open class YandexPlacemarkManager<Model>: BasePlacemarkManager<YMKPlacemarkMapObject, Model, YMKPoint>, YMKMapObjectTapListener {
open class YandexPlacemarkManager<Model>: BasePlacemarkManager<YMKPlacemarkMapObject, Model, YMKPoint>,
YMKMapObjectTapListener {
/// Point (coordinates) itself of the current placemark manager
public let position: YMKPoint
/// Placemark itself of the current placemark manager
public private(set) var placemark: YMKPlacemarkMapObject?
/// The current state of a manager's placemark
public var state: MarkerState = .default {
didSet {
guard let placemark = placemark else {
return
}
if let customIcon = iconFactory?.markerIcon(for: dataModel, state: state) {
placemark.setIconWith(customIcon)
}
}
}
public init(dataModel: Model,
position: YMKPoint,
@ -41,16 +60,26 @@ open class YandexPlacemarkManager<Model>: BasePlacemarkManager<YMKPlacemarkMapOb
// MARK: - YMKMapObjectTapListener
public func onMapObjectTap(with mapObject: YMKMapObject, point: YMKPoint) -> Bool {
tapHandler?(dataModel, point) ?? false
// Tap handling and receiving a result flag
let isTapHandled = tapHandler?(dataModel, point) ?? false
// If a tap was handled successfully then additionally update pin state for appearance configuration
if isTapHandled {
state = .selected
}
return isTapHandled
}
// MARK: - PlacemarkManager
override open func configure(placemark: YMKPlacemarkMapObject) {
// Setting initial values in cluster configuration and each placemark respectively
self.placemark = placemark
self.state = .default
// Setting tap listener for a pin tap handling
placemark.addTapListener(with: self)
if let customIcon = iconFactory?.markerIcon(for: dataModel) {
placemark.setIconWith(customIcon)
}
}
}