I’ve efficiency points with shifting, zooming map with customized markers clusters.
I attempted utilizing greatest practices: keep away from re-render, batch, debounce clusters, all loading and resizing duties made asynchronous and all ui updates in major thread, however nonetheless have lags and snooze map efficiency.
If anybody has any suggestions, I might admire it.
That is MarkerPostItem.swift:
class MarkerPostItem: NSObject, GMUClusterItem {
var place: CLLocationCoordinate2D
var submit: PhotoPost
var picture: PFFileObject
var imageUrl: URL?
var uiImage: BehaviorRelay = .init(worth: nil)
personal let disposeBag = DisposeBag()
personal let imageLoader: ImageLoaderProtocol = ImageLoader()
init(place: CLLocationCoordinate2D, submit: PhotoPost, picture: PFFileObject) {
self.place = place
self.submit = submit
self.picture = picture
self.imageUrl = URL(string: picture.url ?? "")
tremendous.init()
self.fetchAndResizeImage()
}
personal func fetchAndResizeImage() {
imageLoader.loadImage(from: imageUrl, placeholder: UIImage(named: "google-icon")) { [weak self] picture in
let resizedImage = picture?.resizeImage(to: 150.0)
self?.uiImage.settle for(resizedImage)
}
}
}
That is CustomPostClusterRenderer.swift
class CustomPostClusterRenderer: GMUDefaultClusterRenderer {
personal let GMUAnimationDuration: Double = 0.2
personal weak var mapView: GMSMapView?
override init(mapView: GMSMapView, clusterIconGenerator iconGenerator: GMUClusterIconGenerator) {
tremendous.init(mapView: mapView, clusterIconGenerator: iconGenerator)
self.mapView = mapView
self.animationDuration = GMUAnimationDuration
}
override func shouldRender(as cluster: GMUCluster, atZoom zoom: Float) -> Bool {
return cluster.rely >= 1
}
}
That is MapPostClusterIconGenerator.swift:
class MapPostClusterIconGenerator: GMUDefaultClusterIconGenerator {
override func icon(forSize dimension: UInt) -> UIImage {
return generateIcon(forSize: dimension)
}
personal func generateIcon(forSize dimension: UInt) -> UIImage {
let iconImage = #imageLiteral(resourceName: "google-icon")
let textual content = String(dimension) as NSString
let font = UIFont.Spontivly.Khula.extraBold(16.0)
return createIconImage(withText: textual content, baseImage: iconImage, font: font) ?? iconImage
}
personal func createIconImage(withText textual content: NSString, baseImage: UIImage, font: UIFont) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(baseImage.dimension, false, 0.0)
defer { UIGraphicsEndImageContext() }
baseImage.draw(in: CGRect(origin: .zero, dimension: baseImage.dimension))
let textStyle = NSMutableParagraphStyle()
textStyle.alignment = .heart
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.paragraphStyle: textStyle,
.foregroundColor: UIColor.white
]
let textHeight = font.lineHeight
let textY = (baseImage.dimension.peak - textHeight) / 2
let textRect = CGRect(x: 10, y: textY, width: baseImage.dimension.width - 20, peak: textHeight)
textual content.draw(in: textRect.integral, withAttributes: attributes)
return UIGraphicsGetImageFromCurrentImageContext()
}
}
That is ClusterMarkerView.swift:
class ClusterMarkerView: LoadableView {
@IBOutlet weak var iconView: UIImageView!
@IBOutlet weak var quantity: UILabel!
@IBOutlet weak var numberContainer: UIView!
@IBOutlet weak var numberContainerConstraint: NSLayoutConstraint!
personal var url: URL?
personal let disposeBag: DisposeBag = .init()
func replace(quantity: UInt, ratio: CGFloat, uiImage: BehaviorRelay?) {
if let uiImage = uiImage {
uiImage.asDriver().drive(iconView.rx.picture).disposed(by: disposeBag)
} else {
self.iconView.sd_cancelCurrentImageLoad()
loadImage(ratio)
}
DispatchQueue.major.async {
self.numberContainer.isHidden = quantity == 1
self.numberContainerConstraint.fixed = quantity == 1 ? 0.0 : 11.0
self.quantity.textual content = String(quantity)
self.layoutIfNeeded()
}
}
personal func loadImage(_ ratio: CGFloat) {
guard let url = url else { return }
iconView.sd_setImage(with: url,
placeholderImage: nil,
choices: [.continueInBackground, .scaleDownLargeImages],
context: [.imagePreserveAspectRatio : false,
.imageThumbnailPixelSize : CGSize(width: 100, height: (100.0 * ratio))],
progress: nil) { picture, error, cacheType, url in
if let error = error {
print("Error loading picture: (error.localizedDescription)")
}
}
}
}
That is MapViewController.swift delegates:
class MapViewController: UIViewController, NonReusableViewProtocol {
personal var mapView: GMSMapView!
personal var alreadyLoadedPosts: Bool = false
personal var isRenderingCluster: Bool = false
personal var postMarkers = [String: MarkerPostItem]()
personal var renderedPostIds = Set()
personal var renderWorkItem: DispatchWorkItem?
personal var isMapMoving = false
let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
var clusterImage: PFFileObject?
var clusterItemCount: UInt = 0
var firstLoad: Bool = true
var filterISOpened: Bool = false
var searchOpened: Bool = false
var ratio: CGFloat = 0.0
var clusterUiImage: BehaviorRelay?
var postClusterManager: GMUClusterManager?
var subscriptionChecker = Injected()
lazy personal var currentLocationMarker: GMSMarker = {
let currentLocationMarker = GMSMarker()
currentLocationMarker.map = mapView
currentLocationMarker.icon = UIImage(named: "user_pin_icon")
return currentLocationMarker
}()
lazy var renderer: CustomPostClusterRenderer = {
let iconGenerator = MapPostClusterIconGenerator()
let renderer = CustomPostClusterRenderer(mapView: mapView, clusterIconGenerator: iconGenerator)
renderer.delegate = self
return renderer
}()
personal func configurePostMarkerCluster() {
DispatchQueue.international(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
self.postClusterManager = GMUClusterManager(map: self.mapView, algorithm: self.algorithm, renderer: self.renderer)
DispatchQueue.major.async {
self.postClusterManager?.setDelegate(self, mapDelegate: self)
self.postClusterManager?.cluster()
}
}
}
personal func handleFilteredPosts(_ posts: [PhotoPost]) {
guard !posts.isEmpty else { return }
DispatchQueue.international(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
// Set of submit IDs within the present filter outcomes
let currentPostIds = Set(posts.compactMap { $0.objectId })
// Determine markers to take away (these not within the present filter outcomes)
let postsToRemove = self.renderedPostIds.subtracting(currentPostIds)
postsToRemove.forEach { postId in
if let marker = self.postMarkers[postId] {
self.postClusterManager?.take away(marker)
self.postMarkers.removeValue(forKey: postId)
}
}
// Replace `renderedPostIds` to match the present filter
self.renderedPostIds = currentPostIds
// Determine new posts so as to add (these in `currentPostIds` however not but rendered)
let newPosts = posts.filter { submit in
guard let postId = submit.objectId else { return false }
return !self.postMarkers.keys.comprises(postId)
}
// Kind and add new markers for these posts
let sortedPosts = newPosts.sorted { ($0.createdAt ?? Date()) > ($1.createdAt ?? Date()) }
sortedPosts.forEach { submit in
guard let postId = submit.objectId else { return }
let lat = submit.location?.latitude ?? 0
let lng = submit.location?.longitude ?? 0
let marker = MarkerPostItem(place: CLLocationCoordinate2D(latitude: lat, longitude: lng), submit: submit, picture: submit.photoLow)
self.postMarkers[postId] = marker
self.postClusterManager?.add(marker)
}
// Cluster on the principle thread after updating markers
DispatchQueue.major.async {
self.batchCluster()
}
}
}
personal func updateLocation(_ location: CLLocation) {
mapView.animate(toLocation: location.coordinate)
}
personal func updateMapPosition(coordinate: CLLocationCoordinate2D) {
mapView.animate(toLocation: coordinate)
}
}
// MARK: - Delegates
extension MapViewController: GMUClusterRendererDelegate, GMUClusterManagerDelegate, GMSMapViewDelegate {
func clusterManager(_ clusterManager: GMUClusterManager, didTap cluster: GMUCluster) -> Bool {
closeOpenPanelsIfNeeded()
if let gadgets = cluster.gadgets as? [MarkerPostItem] {
viewModel?.selectedCluster.publish(gadgets)
}
return true
}
func renderer(_ renderer: GMUClusterRenderer, markerFor object: Any) -> GMSMarker? {
guard let cluster = object as? GMUStaticCluster,
let postMarker = cluster.gadgets.first as? MarkerPostItem else {
return nil
}
return createMarker(for: cluster, with: postMarker)
}
func renderer(_ renderer: GMUClusterRenderer, willRenderMarker marker: GMSMarker) {
scheduleClusterRenderReset(marker)
}
func renderer(_ renderer: GMUClusterRenderer, didRenderMarker marker: GMSMarker) {
finalizeClusterRendering()
}
func mapViewDidFinishTileRendering(_ mapView: GMSMapView) {
guard !alreadyLoadedPosts else { return }
alreadyLoadedPosts = true
viewModel?.loadPostsMap()
}
func mapView(_ mapView: GMSMapView, didChange place: GMSCameraPosition) {
isMapMoving = true
debounceMapClusterUpdate(for: place)
}
func mapView(_ mapView: GMSMapView, idleAt place: GMSCameraPosition) {
isMapMoving = false
batchCluster() // Set off clustering when motion stops
}
}
// MARK: - Non-public Helper Strategies
personal extension MapViewController {
/// Shut open UI panels corresponding to search or filter
func closeOpenPanelsIfNeeded() {
if searchOpened { closeSearchButton.sendActions(for: .touchUpInside) }
if filterISOpened { filterButton.sendActions(for: .touchUpInside) }
}
/// Create and configure a marker for the given cluster and submit merchandise
func createMarker(for cluster: GMUStaticCluster, with postMarker: MarkerPostItem) -> GMSMarker? {
let marker = GMSMarker()
let view = ClusterMarkerView(body: CGRect(x: 0, y: 0, width: 80, peak: 103))
view.replace(quantity: cluster.rely, ratio: postMarker.submit.imageRatio, uiImage: postMarker.uiImage)
marker.iconView = view
marker.groundAnchor = CGPoint(x: 0.5, y: 1)
return marker
}
/// Schedule a reset for cluster rendering standing
func scheduleClusterRenderReset(_ marker: GMSMarker) {
isRenderingCluster = true
renderWorkItem?.cancel()
renderWorkItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
self.isRenderingCluster = false
}
DispatchQueue.major.asyncAfter(deadline: .now() + 0.5, execute: renderWorkItem!)
}
/// Finalize cluster rendering by hiding loading indicators
func finalizeClusterRendering() {
isRenderingCluster = false
DispatchQueue.major.asyncAfter(deadline: .now() + 2.0) {
if !self.isRenderingCluster, self.alreadyLoadedPosts {
self.loadingView.isHidden = true
self.activityIndicator.stopAnimating()
self.loadingOverlayView.isHidden = true
}
}
}
/// Debounce map clustering updates to keep away from frequent recalculations
func debounceMapClusterUpdate(for place: GMSCameraPosition) {
let significantZoomChange = abs(mapView.digicam.zoom - place.zoom) >= 1.0
if significantZoomChange {
batchCluster()
}
}
personal func batchCluster() {
// Clusters markers provided that map is just not presently shifting
guard !isMapMoving else { return }
postClusterManager?.cluster()
}
}