MKMapView
Annotations
Asynchronous Programming
iOS Development
Swift

How to add annotations to MKMapView asynchronously?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Adding many annotations to MKMapView can cause UI lag if coordinate parsing, filtering, or object creation happens on the main thread. The correct approach is doing heavy preparation in the background, then applying map updates on the main thread. This guide shows a practical asynchronous pipeline for smooth map rendering.

Define a Lightweight Annotation Model

Use a small data model for incoming payloads.

swift
1import Foundation
2import CoreLocation
3
4struct PlaceDTO {
5    let id: String
6    let title: String
7    let latitude: Double
8    let longitude: Double
9}

Convert raw network objects into this model first, then map to annotations.

Create Custom Annotation Type

swift
1import MapKit
2
3final class PlaceAnnotation: NSObject, MKAnnotation {
4    let placeId: String
5    let title: String?
6    let coordinate: CLLocationCoordinate2D
7
8    init(placeId: String, title: String, coordinate: CLLocationCoordinate2D) {
9        self.placeId = placeId
10        self.title = title
11        self.coordinate = coordinate
12    }
13}

A custom type helps with tap handling and diff updates.

Build Annotations Off the Main Thread

Prepare annotation objects in background task.

swift
1func buildAnnotations(from places: [PlaceDTO]) async -> [PlaceAnnotation] {
2    await Task.detached(priority: .userInitiated) {
3        places.compactMap { p in
4            guard abs(p.latitude) <= 90, abs(p.longitude) <= 180 else { return nil }
5            return PlaceAnnotation(
6                placeId: p.id,
7                title: p.title,
8                coordinate: CLLocationCoordinate2D(latitude: p.latitude, longitude: p.longitude)
9            )
10        }
11    }.value
12}

Input validation during background processing avoids invalid coordinate crashes.

Add to Map on Main Thread

All MKMapView updates must happen on the main thread.

swift
1@MainActor
2func applyAnnotations(_ annotations: [PlaceAnnotation], to mapView: MKMapView) {
3    mapView.removeAnnotations(mapView.annotations)
4    mapView.addAnnotations(annotations)
5}

Then wire together in async workflow.

swift
1func refreshMap(mapView: MKMapView, places: [PlaceDTO]) {
2    Task {
3        let annotations = await buildAnnotations(from: places)
4        await applyAnnotations(annotations, to: mapView)
5    }
6}

This pattern keeps UI responsive while processing large datasets.

Reduce Work with Incremental Updates

For frequent updates, avoid full remove-and-add cycles. Diff by identifier and only apply changes.

swift
1func diffIds(old: [PlaceAnnotation], new: [PlaceAnnotation]) -> (remove: [PlaceAnnotation], add: [PlaceAnnotation]) {
2    let oldIds = Set(old.map { $0.placeId })
3    let newIds = Set(new.map { $0.placeId })
4
5    let toRemove = old.filter { !newIds.contains($0.placeId) }
6    let toAdd = new.filter { !oldIds.contains($0.placeId) }
7    return (toRemove, toAdd)
8}

Incremental updates reduce main-thread churn and visual flicker.

Enable Clustering for Dense Maps

Large annotation sets can be made smoother using clustering.

swift
1class PlaceAnnotationView: MKMarkerAnnotationView {
2    override var annotation: MKAnnotation? {
3        willSet {
4            clusteringIdentifier = "place"
5        }
6    }
7}

Register view and reuse identifiers to keep rendering efficient.

Fetch Remote Data Asynchronously Before Mapping

Network requests should complete before annotation conversion begins. Keep parsing and conversion off main thread.

swift
1func fetchPlaces() async throws -> [PlaceDTO] {
2    let url = URL(string: "https://example.com/places.json")!
3    let (data, _) = try await URLSession.shared.data(from: url)
4    return try JSONDecoder().decode([PlaceDTO].self, from: data)
5}

Then chain fetch and map update in one task.

swift
1Task {
2    do {
3        let places = try await fetchPlaces()
4        let annotations = await buildAnnotations(from: places)
5        await applyAnnotations(annotations, to: mapView)
6    } catch {
7        print(error)
8    }
9}

Debounce Rapid Update Bursts

If location feeds update frequently, debounce refresh requests to avoid repeated annotation churn.

A simple approach is cancelling previous task and scheduling a new update task. This keeps map interaction smooth during fast data bursts.

Profile map updates with Instruments when scaling annotation counts, then tune diffing and clustering thresholds based on measured frame drops.

This simple policy improves responsiveness during rapid pan and zoom operations.

Measure, tune, and verify after each change.

Common Pitfalls

  • Creating thousands of annotations on the main thread.
  • Updating MKMapView from background threads.
  • Rebuilding and re-adding all annotations for minor data changes.
  • Skipping coordinate validation and inserting invalid locations.
  • Ignoring clustering when map density is high.

Summary

  • Do heavy annotation preparation asynchronously off the main thread.
  • Apply map updates only on the main thread.
  • Use custom annotation types for clear identity and reuse.
  • Prefer incremental diff updates over full replacement.
  • Enable clustering and reuse to maintain smooth map interaction.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.