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, v10.5.0, in the Maps SDK documentation.
This example requires multiple files: three for Objective-C, two for Swift.
CustomCalloutView
implements theMGLCalloutView
protocol.CustomCalloutViewController
uses the custom callout withMGLMapView
.
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 = falselet isAnchoredToAnnotation: Bool = true // https://github.com/mapbox/mapbox-gl-native/issues/9228override var center: CGPoint {set {var newCenter = newValuenewCenter.y -= bounds.midYsuper.center = newCenter}get {return super.center}} lazy var leftAccessoryView = UIView() /* unused */lazy var rightAccessoryView = UIView() /* unused */ weak var delegate: MGLCalloutViewDelegate? let tipHeight: CGFloat = 10.0let tipWidth: CGFloat = 20.0 let mainBody: UIButton required init(representedObject: MGLAnnotation) {self.representedObject = representedObjectself.mainBody = UIButton(type: .system) super.init(frame: .zero) backgroundColor = .clear mainBody.backgroundColor = .darkGraymainBody.tintColor = .whitemainBody.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 APIfunc 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.widthlet frameHeight = mainBody.bounds.size.height + tipHeightlet frameOriginX = rect.origin.x + (rect.size.width/2.0) - (frameWidth/2.0)let frameOriginY = rect.origin.y - frameHeightframe = CGRect(x: frameOriginX, y: frameOriginY, width: frameWidth, height: frameHeight) if animated {alpha = 0 UIView.animate(withDuration: 0.2) { [weak self] inguard let strongSelf = self else {return} strongSelf.alpha = 1strongSelf.delegate?.calloutViewDidAppear?(strongSelf)}} else {delegate?.calloutViewDidAppear?(self)}} func dismissCallout(animated: Bool) {if (superview != nil) {if animated {UIView.animate(withDuration: 0.2, animations: { [weak self] inself?.alpha = 0}, completion: { [weak self] _ inself?.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 = .darkGrayview.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)}}