MapKit — Next Level Map Experience

Cagrigider
adessoTurkey
Published in
6 min readSep 11, 2023

Take your app’s user experience to the next level using custom annotations and clustering.

Map with custom annotations and custom cluster annotations

Today, maps are used in most applications. Even if the map does not constitute the entire application, it takes up a very important place in applications. For this reason, it is very important to use the map and map features correctly in practice. The pin design and colors to be displayed on the map should be specific to the application, ensuring cohesion with the application and making the map pleasing to the eye.

Let’s go over how to use a map in an iOS app.

Map Setup

First, we add the map to our storyboard, which belongs to our ViewController that we will use. Then we complete the connections.

After we finish setting up the storyboard, we can continue preparing our view controller class.

For this, we first create a function called “setupMapView”. In this function, we set the required properties of the map and the delegate. We also set the location where we want the map to open. Since we do not take the user’s location, we give a fixed point for testing.

private func setupMapView() {
mapView.delegate = self
mapView.showsUserLocation = false

let region = MKCoordinateRegion(center: .init(latitude: 35.278259, longitude: -119.5), latitudinalMeters: 500000, longitudinalMeters: 500000)
mapView.setRegion(region, animated: false)
}

After this, we create a CustomAnnotation class and two new functions in the ViewController.swift to create example annotation data to show on the map.

private func addExamplePoints() {
/// Removing existing annotations before adding new ones
let oldAnnotations = self.mapView.annotations
mapView.removeAnnotations(oldAnnotations)

/// Generating and adding new annotations to the map
let points = generateExamplePoints(40)
mapView.addAnnotations(points)
}

private func generateExamplePoints(_ pointsCount: Int) -> [CustomAnnotation] {
var points: [CustomAnnotation] = []

/// Generating sample points based on the given count
for _ in 0..<pointsCount {
let latitude = Double.random(in: 34.0 ... 36.0)
let longitude = Double.random(in: -121.0 ... -119.0)
let annotation = CustomAnnotation(latitude: latitude, longitude: longitude)
points.append(annotation)
}

return points
}
import MapKit

class CustomAnnotation: NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D

init(latitude: Double, longitude: Double){
self.coordinate = CLLocationCoordinate2DMake(latitude, longitude)
}
}

After adding our new class and new functions, we can call setupMapView, addExamplePoints functions in the viewDidLoad function of our ViewController.swift file.

override func viewDidLoad() {
super.viewDidLoad()

setupMapView()
addExamplePoints()
}

Now we can create our MKMapViewDelegate with ViewController class file, to reach map features and complete the initial setup of our map.

We simply create a new file named ViewController+Map.swift, and in the file, we create our ViewController extension to use MKMapViewDelegate functions as shown in the below code.

import MapKit

extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
}
return MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
}

After this, we can run and see the first output of our map.

Map with example pins.

Now we can start customizing our map and user experience.

Custom Annotations

To use custom annotations on the map in Swift, we need to create a custom class from MKAnnotationView. For this project, we will create a CustomAnnotationView.swift file, and for the design of the annotation, we will create a CustomAnnotationView.xib file. Also, for making our imageView circle, we will create a IBDesignable class for UIImageView.

import MapKit

class CustomAnnotationView: MKAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
commonInit()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func commonInit() {
let view = self.nibInstantiate(autoResizingMask: [.flexibleHeight, .flexibleWidth])
self.frame = view.frame
addSubview(view)
}
}
import UIKit

@IBDesignable
class CircleImageView: UIImageView {

@IBInspectable
var circle: Bool {
set {
self.isCircle = newValue
self.setCircle()
}
get { self.isCircle }
}

private var isCircle: Bool = false

private func setCircle() {
if isCircle {
layer.cornerRadius = bounds.size.width / 2
}
}
}
CustomAnnotationView.xib

Now we need to tell the map that we will use our new CustomAnnotationView instead of the default marker on the map. To do this, we need to edit the delegate function in the ViewController as shown below.

import MapKit

extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
}

if annotation is CustomAnnotation {
guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier) as? CustomAnnotationView else {
return CustomAnnotationView(annotation: annotation, reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
annotationView.annotation = annotation
return annotationView
} else {
return MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
}
}

And now we can see our beautiful custom pins.

Map with custom pins

Clustering

Another feature that needs to be done for more visually pleasing and easy-to-use maps is “Clustering”. Clustering is enabled by default for the default markers of Apple. However, since we are using custom annotations, we need to create custom clustered annotations.

To use the clustering feature, we will first create our CustomClusterAnnotationView.swift and CustomClusterAnnotationView.xib files. We will also add a UILabel to our custom-designed pins to show how many pins are clustered under this cluster pin.

import UIKit
import MapKit

class CustomClusterAnnotationView: MKAnnotationView {
@IBOutlet private weak var countLabel: UILabel!

override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
let clusterAnnotation = annotation as? MKClusterAnnotation
commonInit()
setUI(with: clusterAnnotation)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func commonInit() {
let view = self.nibInstantiate(autoResizingMask: [.flexibleHeight, .flexibleWidth])
self.frame = view.frame
addSubview(view)
}

private func setUI(with clusterAnnotation: MKClusterAnnotation?) {
if let count = clusterAnnotation?.memberAnnotations.count {
countLabel.text = count.description
} else {
countLabel.text = nil
countLabel.isHidden = true
}
}
}
CustomClusterAnnotationView.xib

After this, we need to change some code in our MapView delegate function. Then we need to identify that all our pins can be clustered under the same pin, because we have only one type of annotation now.

import MapKit

extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
}

if annotation is CustomAnnotation {
guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier) as? CustomAnnotationView else {
return CustomAnnotationView(annotation: annotation, reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
annotationView.annotation = annotation
annotationView.clusteringIdentifier = MKMapViewDefaultClusterAnnotationViewReuseIdentifier
return annotationView
} else if annotation is MKClusterAnnotation {
return CustomClusterAnnotationView(annotation: annotation, reuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)
} else {
return MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
}
}

Now, we will be able to see our annotations clustered when we zoom out of the map, as shown below.

Final looking of the map

Conclusion

As you can see, we have brought the usability and readability of the map used in the application to the next level in a short time. You can make different designs for pins and clustered pins and take your application further.

Today, the designs of applications play a very important role, so fine details in this way will positively affect both the memorability and the user experience.

You can review the codes of the application via the link below.

Happy Coding!

https://github.com/cagrigider/Map-UIKit

--

--