MKAnnotationView
callout bubble
customization
iOS development
map annotations

How to customize the callout bubble for MKAnnotationView?

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

MapKit gives MKAnnotationView a built-in callout, but that default bubble is intentionally limited. You can customize parts of it with accessory views, and if you need a fully branded or interactive bubble, the usual solution is to disable the standard callout and present your own custom view.

What the Default Callout Supports

The built-in callout is easy to enable:

swift
1import MapKit
2
3final class MapViewController: UIViewController, MKMapViewDelegate {
4    @IBOutlet private weak var mapView: MKMapView!
5
6    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
7        let identifier = "Pin"
8        let view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
9            as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
10
11        view.annotation = annotation
12        view.canShowCallout = true
13        view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
14
15        return view
16    }
17}

That gives you the standard bubble with a disclosure button on the right side.

Accessory Views Are the First Level of Customization

If the default bubble is mostly good enough, you can add content around it with accessory views.

swift
1let imageView = UIImageView(image: UIImage(systemName: "star.fill"))
2imageView.tintColor = .systemYellow
3view.leftCalloutAccessoryView = imageView
4
5let subtitleLabel = UILabel()
6subtitleLabel.text = "Open details"
7subtitleLabel.font = .systemFont(ofSize: 12)
8view.detailCalloutAccessoryView = subtitleLabel

This approach keeps native MapKit behavior while still letting you add small amounts of custom UI.

When the Default Bubble Is Not Enough

If you want custom layout, animations, large images, or fully controlled interactions, the standard callout becomes restrictive. In that case:

  1. set canShowCallout to false
  2. subclass MKAnnotationView or use a custom annotation view
  3. show your own bubble view when the annotation is selected

This is the common pattern for highly customized map annotations.

A Custom Bubble Example

Here is a basic custom annotation view that shows a separate bubble subview.

swift
1import MapKit
2
3final class CustomAnnotationView: MKAnnotationView {
4    private let bubbleView = UIView()
5    private let titleLabel = UILabel()
6
7    override var annotation: MKAnnotation? {
8        didSet {
9            titleLabel.text = annotation?.title ?? nil
10        }
11    }
12
13    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
14        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
15        canShowCallout = false
16
17        bubbleView.backgroundColor = .white
18        bubbleView.layer.cornerRadius = 12
19        bubbleView.layer.shadowOpacity = 0.2
20        bubbleView.layer.shadowRadius = 6
21        bubbleView.isHidden = true
22        bubbleView.translatesAutoresizingMaskIntoConstraints = false
23
24        titleLabel.translatesAutoresizingMaskIntoConstraints = false
25
26        addSubview(bubbleView)
27        bubbleView.addSubview(titleLabel)
28
29        NSLayoutConstraint.activate([
30            bubbleView.bottomAnchor.constraint(equalTo: topAnchor, constant: -8),
31            bubbleView.centerXAnchor.constraint(equalTo: centerXAnchor),
32            titleLabel.topAnchor.constraint(equalTo: bubbleView.topAnchor, constant: 8),
33            titleLabel.bottomAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: -8),
34            titleLabel.leadingAnchor.constraint(equalTo: bubbleView.leadingAnchor, constant: 12),
35            titleLabel.trailingAnchor.constraint(equalTo: bubbleView.trailingAnchor, constant: -12)
36        ])
37    }
38
39    required init?(coder: NSCoder) {
40        fatalError("init(coder:) has not been implemented")
41    }
42
43    override func setSelected(_ selected: Bool, animated: Bool) {
44        super.setSelected(selected, animated: animated)
45        bubbleView.isHidden = !selected
46    }
47}

This is a simple starting point for a fully custom bubble.

Handling Taps and Reuse

Custom callouts require more manual work than the default one. You need to think about:

  • reusing annotation views correctly
  • hiding or showing custom subviews on selection changes
  • forwarding touch handling if the bubble contains buttons
  • making sure the bubble does not get clipped by the annotation view hierarchy

That extra control is the benefit and the cost of custom callouts.

Use detailCalloutAccessoryView Before Building Everything Yourself

Many developers jump straight to a fully custom bubble when a detailCalloutAccessoryView would have solved the problem with much less code. If your needs are limited to an image, a subtitle, or a compact custom detail view, stay inside the default callout system first.

That preserves native animations, hit testing, and selection behavior.

Common Pitfalls

The biggest pitfall is trying to restyle the built-in bubble too deeply. MapKit does not expose every part of the default callout for arbitrary visual customization.

Another issue is forgetting that annotation views are reused. If custom bubble state is not reset correctly, old content can appear on the wrong annotation.

Developers also create custom callouts that intercept touches poorly or disappear unexpectedly because setSelected is not handled consistently.

Finally, be careful with layout. A custom bubble that extends outside the annotation view's bounds may need extra attention to clipping and interaction behavior.

Summary

  • Use canShowCallout = true for the standard MapKit bubble.
  • Customize the standard callout with leftCalloutAccessoryView, rightCalloutAccessoryView, and detailCalloutAccessoryView.
  • If you need full visual control, disable the standard callout and present your own bubble view.
  • Handle reuse and selection state carefully in custom annotation views.
  • Prefer the built-in callout unless your design really requires a fully custom bubble.

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.