Skip to main content

Display custom views as callouts

A newer version of the Maps SDK is available

This page uses v6.4.1 of the Mapbox Maps SDK. A newer version of the SDK is available. Learn about the latest version, v11.3.0, in the Maps SDK documentation.

This example requires multiple files: three for Objective-C, two for Swift.

  • CustomCalloutView implements the MGLCalloutView protocol.
  • CustomCalloutViewController uses the custom callout with MGLMapView.

See the MGLCalloutView and MGLCalloutDelegate protocols for more information about creating customized callouts.

Header
import Mapbox

class CustomCalloutView: UIView, MGLCalloutView {
var representedObject: MGLAnnotation

// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true

// https://github.com/mapbox/mapbox-gl-native/issues/9228
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}

lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */

weak var delegate: MGLCalloutViewDelegate?

let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0

let mainBody: UIButton

required init(representedObject: MGLAnnotation) {
self.representedObject = representedObject
self.mainBody = UIButton(type: .system)

super.init(frame: .zero)

backgroundColor = .clear

mainBody.backgroundColor = .darkGray
mainBody.tintColor = .white
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0

addSubview(mainBody)
}

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

// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {

delegate?.calloutViewWillAppear?(self)

view.addSubview(self)

// Prepare title label.
mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.sizeToFit()

if isCalloutTappable() {
// Handle taps and eventually try to send them to the delegate (usually the map view).
mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
} else {
// Disable tapping and highlighting.
mainBody.isUserInteractionEnabled = false
}

// Prepare our frame, adding extra space at the bottom for the tip.
let frameWidth = mainBody.bounds.size.width
let frameHeight = mainBody.bounds.size.height + tipHeight
let frameOriginX = rect.origin.x + (rect.size.width/2.0) - (frameWidth/2.0)
let frameOriginY = rect.origin.y - frameHeight
frame = CGRect(x: frameOriginX, y: frameOriginY, width: frameWidth, height: frameHeight)

if animated {
alpha = 0

UIView.animate(withDuration: 0.2) { [weak self] in
guard let strongSelf = self else {
return
}

strongSelf.alpha = 1
strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
delegate?.calloutViewDidAppear?(self)
}
}

func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.alpha = 0
}, completion: { [weak self] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}

// MARK: - Callout interaction handlers

func isCalloutTappable() -> Bool {
if let delegate = delegate {
if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
return delegate.calloutViewShouldHighlight!(self)
}
}
return false
}

@objc func calloutTapped() {
if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
delegate!.calloutViewTapped!(self)
}
}

// MARK: - Custom view styling

override func draw(_ rect: CGRect) {
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .darkGray

let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1

let currentContext = UIGraphicsGetCurrentContext()!

let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()

fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
ViewController
import Mapbox

class ViewController: UIViewController, MGLMapViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()

let mapView = MGLMapView(frame: view.bounds, styleURL: MGLStyle.lightStyleURL)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.tintColor = .darkGray
view.addSubview(mapView)

// Set the map view‘s delegate property.
mapView.delegate = self

// Initialize and add the marker annotation.
let marker = MGLPointAnnotation()
marker.coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)
marker.title = "Hello world!"

// This custom callout example does not implement subtitles.
//marker.subtitle = "Welcome to my marker"

// Add marker to the map.
mapView.addAnnotation(marker)

// Select the annotation so the callout will appear.
mapView.selectAnnotation(marker, animated: false, completionHandler: nil)
}

func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
// Only show callouts for `Hello world!` annotation.
return annotation.responds(to: #selector(getter: MGLAnnotation.title)) && annotation.title! == "Hello world!"
}

func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
// Instantiate and return our custom callout view.
return CustomCalloutView(representedObject: annotation)
}

func mapView(_ mapView: MGLMapView, tapOnCalloutFor annotation: MGLAnnotation) {
// Optionally handle taps on the callout.
print("Tapped the callout for: \(annotation)")

// Hide the callout.
mapView.deselectAnnotation(annotation, animated: true)
}
}