Simplifying MKMapView Overlays

Alex-Stefan Albu
Yonder TechBlog
Published in
7 min readFeb 7, 2024

--

A New Approach to a Common Problem

Photo by Tamas Tuzes-Katai on Unsplash

Hello fellow developers! Navigating the intricacies of MKMapView overlays just got simpler. Today, I’m thrilled to introduce a project crafted to handle overlays with grace on MKMapView. This solution is all about bringing simplicity, boosting performance, and delivering a seamless user experience. Here’s how:

Streamlined Code Logic:

  • Condensed complex logic into straightforward and comprehensible structures.
  • Eliminated redundant or convoluted steps, making the code more efficient.

Reduced Dependencies:

  • Minimised external dependencies, focusing on native Swift capabilities.
  • Stripped away unnecessary layers or frameworks, enhancing performance.

Seamless integration:

  • Ensured smooth integration of features, avoiding conflicts or disruptions.
  • Prioritised cohesion and compatibility within the project.

Emphasis on Core Functionality:

  • Focused on the core functionalities of the application, reducing complexity.

In a world of mobile applications that use complex(and sometimes unnecessary) solutions, this project stands out by offering an uncomplicated yet powerful approach to managing overlays.

What are overlays and where are they used?

Before we dive into the project’s features, let’s understand what overlays are and where they find their utility. In the context of maps and graphical user interfaces, overlays are additional graphical elements or layers placed on top of an existing map or interface. These elements provide extra information or features, enhancing the user experience. Overlays can take various forms, such as markers, lines, shapes, or other visual components.

Common use cases for Overlays:

  1. Markers and Pins:
  • Use: To indicate specific locations on a map.
  • Example: Showing the location of restaurants, landmarks, or points of interest.

2. Polygons and Shapes:

  • Use: To highlight and define areas on a map.
  • Example: Outlining city boundaries, parks, or regions of interest.

3. Polylines and Routes:

  • Use: To display paths or routes on a map.
  • Example: Showing directions, hiking trails, or biking routes.

4. Info Windows:

  • Use: To provide additional information when a user interacts with a map element.
  • Example: Displaying details about a place when a user clicks on a marker.

5. Heatmaps:

  • Use: To visualize the intensity or density of data in a particular area.
  • Example: Displaying the popularity of locations, such as where people are taking photos.

6. Tile Overlays:

  • Use: To overlay custom images or map layers.
  • Example: Adding weather patterns or satellite imagery as an overlay.

7. Augmented Reality Overlays:

  • Use: To overlay digital information on the real world through a device’s camera.
  • Example: AR navigation apps that display directions on the live camera feed.

Overlays are versatile tools used in mapping applications, providing a visual means to convey diverse types of information and improving the overall usability of location-based services.

Efficient Use of Overlays:

To use overlays efficiently, consider the following principles:

  1. Minimalism: Use overlays only when absolutely necessary. Avoid cluttering the map with too many elements, which can lead to visual confusion. Focus on displaying essential information.
  2. Optimised Rendering: Optimise the rendering of overlay elements for speed. Use efficient algorithms and data structures to ensure quick updates and interactions.
  3. Asynchronous Loading: Load overlay data asynchronously to prevent blocking the main thread. This ensures that the map remains responsive even when dealing with large datasets.
  4. Dynamic Loading: Load overlays dynamically based on user interactions and the area currently visible on the map. This helps in conserving resources and maintaining smooth performance.
  5. Caching: Implement caching mechanisms for overlay data to reduce redundant requests. This is especially crucial when dealing with dynamic data that doesn’t change frequently.

Now, armed with this knowledge of good practices and principles, let’s explore the key features of our project.

Key Features:

📐 Dynamic Sizing:

No more guessing! This project calculates the perfect size for your overlay on a snapshot, making it hassle-free.

🗺️ Tailored for MKMapView:

Built for MKMapView overlays, it seamlessly blends with your maps, enhancing functionality without sacrificing speed.

🚀 Swift Compatibility:

Built with Swift 5, for efficiency and modernity in one.

📸 Snapshot Autoscroller:

Snapshots made easy! Store them securely with SwiftData, and scroll through your tracking history effortlessly.

Implementation details

The TrackingResults Class:

In the heart of our project lies the TrackingResults class. This class is responsible for storing snapshots of tracked data efficiently. Let's break down its components:

  • @Model: This annotation suggests that the class is a model, representing a data structure to be stored persistently.
  • @Attribute(.unique) var id: UUID: An attribute representing a unique identifier for each instance of TrackingResults. This ensures that each tracked result has a distinct identity.
  • var mapSnapshot: String: This property holds the map snapshot data as a string. It represents an encoded image.
import SwiftData
import Foundation

@Model
class TrackingResults {
@Attribute(.unique) var id: UUID
var mapSnapshot: String

init(id: UUID, mapSnapshot: String) {
self.id = id
self.mapSnapshot = mapSnapshot
}
}

Initialising ModelContainerat the App Level:

In the provided Swift code, the ConformingOverlayRegionApp struct represents the main application. The critical part here is the initialization of ModelContainer for the TrackingResults class. Let's understand why this is essential.

@main
struct ConformingOverlayRegionApp: App {
let modelContainer: ModelContainer

init() {
do {
modelContainer = try ModelContainer(for: TrackingResults.self)
} catch {
fatalError("Could not initialize ModelContainer")
}
}

var body: some Scene {
WindowGroup {
MainView()
.modelContainer(modelContainer)
}
}
}

By initialising ModelContainer for the TrackingResults class at the app level, you establish a central hub for managing the persistence of tracked results.

Core feature of the application: MapViewModel (Github repository will be linked below):

Let’s dive into the detailed explanation of the key methods and extensions in the MapViewModel class:

1. drawLine()

func drawLine() {
mapView?.setUserTrackingMode(userTrackingMode, animated: true)
let polyline = MKPolyline(coordinates: locations, count: locations.count)
if let overlay = mapView?.overlays.first {
mapView?.removeOverlay(overlay)
}
mapView?.addOverlay(polyline)
}

This method updates the map view by drawing a polyline based on the current set of location coordinates (locations). It first sets the user tracking mode to ensure the map follows the user's location. Then, it creates an MKPolyline object using the collected coordinates. If there's already an overlay on the map, it removes it to refresh the display. Finally, it adds the new polyline overlay to the map.

2. createSnapshot()

func createSnapshot() async -> String? {
setupSnapshotter()
let snapshotter = MKMapSnapshotter(options: options)
do {
let imageSnapshot = try await snapshotter.start()
let pointsOnImage = locations.map { imageSnapshot.point(for: $0) }
let image = UIImage.drawPointsOnImage(imageSnapshot.image, points: pointsOnImage)
let imageData = image.jpegData(compressionQuality: 0.8)?.base64EncodedString()
return imageData
} catch {
print("Something went wrong: \(error.localizedDescription)")
return nil
}
}

This asynchronous method creates a map snapshot using MKMapSnapshotter. It sets up the snapshotter options, starts the snapshot, and converts it into a UIImage. It then maps the location points to points on the image, draws them on the image, and converts the resulting image to a base64-encoded string. Any errors during this process are caught and printed.

3. onDisappear(context: ModelContext) async

func onDisappear(context: ModelContext) async {
deallocateManagers()
let generatedSnapshotInBase64 = await createSnapshot() ?? ""
let generatedUUID = UUID()

createTrackingResultsObject(id: generatedUUID, base64Image: generatedSnapshotInBase64, for: context)
}

This method is called when the associated view disappears. It first deallocates location managers, then asynchronously generates a map snapshot. After obtaining the snapshot, it creates a new UUID, and finally, it creates a TrackingResults object with the generated UUID and snapshot, saving it to the provided ModelContext.

4. calculateRegionToFitOverlay(_:)

private func calculateRegionToFitOverlay(_ coordinates: [CLLocationCoordinate2D]) -> MKCoordinateRegion {
guard !coordinates.isEmpty else {
return MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 52.239647, longitude: 21.045845),
span: MKCoordinateSpan(latitudeDelta: DEFAULT_REGION_SPAN, longitudeDelta: DEFAULT_REGION_SPAN)
)
}

let latitudes = coordinates.map { $0.latitude }
let longitudes = coordinates.map { $0.longitude }
let minLat = latitudes.min()!
let maxLat = latitudes.max()!
let minLong = longitudes.min()!
let maxLong = longitudes.max()!

let center = CLLocationCoordinate2D(
latitude: (minLat + maxLat) / 2.0,
longitude: (minLong + maxLong) / 2.0
)

let span = MKCoordinateSpan(
latitudeDelta: maxLat - minLat + 0.01,
longitudeDelta: maxLong - minLong + 0.01
)

return MKCoordinateRegion(center: center, span: span)
}

This private method calculates the optimal MKCoordinateRegion that fits the given array of coordinates. It determines the minimum and maximum latitudes and longitudes from the coordinates, calculates the center and span, and returns the resulting region.

5. CLLocationManagerDelegate Extension

extension MapViewModel: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
checkLocationAuthorization()
}
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let coordinate = manager.location?.coordinate else { return }
DispatchQueue.main.async { [self] in
self.locations.append(coordinate)
self.drawLine()
}
}
}

These extension methods handle changes in location authorisation status and updates in location coordinates, conforming to the CLLocationManagerDelegate protocol.

AutoScroller Feature

The AutoScroller feature in this project is a noteworthy addition designed to enhance the user experience. This feature is authored by Aditya Gautam. You can find the entire post on his Medium profile.

The ScrollerView displays all snapshots of trackings ever taken, stored in the database using SwiftData. It allows users to scroll through snapshots seamlessly, providing a comprehensive view of their tracking history. This feature contributes to a more engaging and interactive exploration of past activities. The version of my code is slightly altered to better fit the context of my application, but, in large, it is the same code:

struct ScrollerView: View {
// MARK: - Properties
let images: [UIImage]
let timer = Timer.publish(every: 3.0, on: .main, in: .common).autoconnect()

// MARK: - State
// Manage Selected Image Index
@State private var selectedImageIndex: Int = 0

var body: some View {
ZStack {
// Background Color
Color.secondary
.ignoresSafeArea()

// Create TabView for Carousel
TabView(selection: $selectedImageIndex) {
// Iterate Through Images
ForEach(0..<images.count, id: \.self) { index in
ZStack(alignment: .topLeading) {
// Display Image
Image(uiImage: images[index])
.resizable()
.tag(index)
.frame(width: 365.625, height: 650)
}
.background(VisualEffectBlur()) // Apply Visual Effect Blur
.shadow(radius: 20) // Apply Shadow
}
}
.frame(height: 650) // Set Carousel Height
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) // Customize TabView Style
.ignoresSafeArea()

// Navigation Dots
HStack {
ForEach(0..<images.count, id: \.self) { index in
// Create Navigation Dots
Capsule()
.fill(Color.white.opacity(selectedImageIndex == index ? 1 : 0.33))
.frame(width: 35, height: 8)
.onTapGesture {
// Handle Navigation Dot Taps
selectedImageIndex = index
}
}
.offset(y: 350) // Adjust Dots Position
}
}
.onReceive(timer) { _ in
// Auto-Scrolling Logic
withAnimation(.default) {
selectedImageIndex = (selectedImageIndex + 1) % images.count
}
}
}
}

Wrapping Up

In closing, the ConformingOverlayRegion project exemplifies efficiency and simplicity in managing MKMapView overlays. From fundamental insights to Swift programming intricacies, this journey encompasses both exploration and implementation.

Github repository: https://github.com/albu-alex/ConformingOverlayRegion

Explore the repository for hands-on experience. Cheers to continuous learning and collaborative innovation! Happy coding! 🤙🏻

--

--