6.9 C
New York
Thursday, November 28, 2024

ios – GoogleMaps customized markers clustering efficiency problem


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()
    }
}

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles