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.9.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 = 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()
}
}
Header
//
// CustomCalloutView.h
// Examples
//
// Created by Jason Wray on 3/6/16.
// Copyright © 2016 Mapbox. All rights reserved.
//
#import <UIKit/UIKit.h>
@import Mapbox;
@interface CustomCalloutView : UIView <MGLCalloutView>
@end
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)
}
}
#import "ViewController.h"
#import "CustomCalloutView.h"
@import Mapbox;
@interface ViewController () <MGLMapViewDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
MGLMapView *mapView = [[MGLMapView alloc] initWithFrame:self.view.bounds styleURL:[MGLStyle lightStyleURL]];
mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
mapView.tintColor = [UIColor darkGrayColor];
[self.view addSubview:mapView];
// Set the map view‘s delegate property.
mapView.delegate = self;
// Initialize and add the marker annotation.
MGLPointAnnotation *marker = [[MGLPointAnnotation alloc] init];
marker.coordinate = CLLocationCoordinate2DMake(0, 0);
marker.title = @"Hello world!";
// This custom callout example does not implement subtitles.
//marker.subtitle = @"Welcome to my marker";
// Add the annotation to the map.
[mapView addAnnotation:marker];
// Select the annotation so the callout will appear.
[mapView selectAnnotation:marker animated:NO completionHandler:nil];
}
- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation {
// Only show callouts for `Hello world!` annotation.
return [annotation respondsToSelector:@selector(title)] && [annotation.title isEqualToString:@"Hello world!"];
}
- (UIView<MGLCalloutView> *)mapView:(__unused MGLMapView *)mapView calloutViewForAnnotation:(id<MGLAnnotation>)annotation
{
// Instantiate and return our custom callout view.
CustomCalloutView *calloutView = [[CustomCalloutView alloc] init];
calloutView.representedObject = annotation;
return calloutView;
}
- (void)mapView:(MGLMapView *)mapView tapOnCalloutForAnnotation:(id<MGLAnnotation>)annotation
{
// Optionally handle taps on the callout.
NSLog(@"Tapped the callout for: %@", annotation);
// Hide the callout.
[mapView deselectAnnotation:annotation animated:YES];
}
@end