Display a popup

Estimated reading time: 5 minutes

Check out this code sample that uses the Mapbox GL iOS library to add a popup over your map data points.

View on GitHub

import UIKit
import Mapbox

class AddPopupViewController: UIViewController, MGLMapViewDelegate {

    // Update the token here if you want to customize the token for this controller in your own project.
    // Otherwise update the value at the top of the main controller: ViewController.swift.
    // let accessToken = "YOUR_ACCESS_TOKEN"

    var mapView = MGLMapView()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Initialize title
        title = "Custom Popup"
        // Initialize map
        let url = URL(string: "https://api.jawg.io/styles/jawg-sunny.json?access-token="+accessToken)
        mapView = MGLMapView(frame: view.bounds, styleURL: url)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.delegate = self
        mapView.logoView.isHidden = true
        // Set the map’s center coordinate and zoom level.
        mapView.setCenter(CLLocationCoordinate2D(latitude: -33.865143, longitude: 151.209900), zoomLevel: 11, animated: false)
        // Add an annotation
        let operaHouseAnnotation = CustomAnnotation(coordinate: CLLocationCoordinate2D(latitude: -33.85416325, longitude: 151.20916),title: "Opera House",subtitle: "The most beautiful opera house in the world!",image: UIImage(named: "operaHouse.jpeg")!)
        mapView.addAnnotation(operaHouseAnnotation)
        // Add an annotation
        let sydneyParkAnnotation = CustomAnnotation(coordinate: CLLocationCoordinate2D(latitude: -33.9105284, longitude: 151.1846209),title: "Sydney Park",subtitle: "The open space is made of 40 hectares of lush grass",image: UIImage(named: "sydneyPark.jpeg")!)
        mapView.addAnnotation(sydneyParkAnnotation)
        // Add an annotation
        let bondiBeachAnnotation = CustomAnnotation(coordinate: CLLocationCoordinate2D(latitude: -33.890842, longitude: 151.274292),title: "Bondi Beach",subtitle: "Do not forget your surfboard",image: UIImage(named: "bondiBeach.jpeg")!)
        mapView.addAnnotation(bondiBeachAnnotation)
        // Add the map to the view
        view.addSubview(mapView)
    }

    // This delegate method is where you tell the map to load a view for a specific annotation. To load a static MGLAnnotationImage, you would use `-mapView:imageForAnnotation:`.
    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        // This example is only concerned with point annotations.
        guard annotation is CustomAnnotation else {
            return nil
        }
        // Use the point annotation’s longitude value (as a string) as the reuse identifier for its view.
        let reuseIdentifier = "\(annotation.coordinate.longitude)"
        // For better performance, always try to reuse existing annotations.
        var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
        // If there’s no reusable annotation view available, initialize a new one.
        if annotationView == nil {
            annotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
            annotationView!.bounds = CGRect(x: 0, y: 0, width: 40, height: 40)
            // Set the annotation view’s background color to a value determined by its longitude.
            let hue = CGFloat(annotation.coordinate.longitude) / 100
            annotationView!.backgroundColor = UIColor(hue: hue, saturation: 0.5, brightness: 1, alpha: 1)
        }
        return annotationView
    }

    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return true
    }

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

    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)
    }
}

class CustomAnnotation: NSObject, MGLAnnotation {

    var coordinate: CLLocationCoordinate2D
    var title: String?
    var subtitle: String?
    var image: UIImage

    init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String, image: UIImage) {
        self.coordinate = coordinate
        self.title = title
        self.subtitle = subtitle
        self.image = image
    }
}

// MGLAnnotationView subclass
class CustomAnnotationView: MGLAnnotationView {

    override init(annotation: MGLAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        // Use CALayer’s corner radius to turn this view into a circle.
        layer.cornerRadius = bounds.width / 2
        layer.borderWidth = 2
        layer.borderColor = UIColor.white.cgColor
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        // Animate the border width in/out, creating an iris effect.
        let animation = CABasicAnimation(keyPath: "borderWidth")
        animation.duration = 0.1
        layer.borderWidth = selected ? bounds.width / 4 : 2
        layer.add(animation, forKey: "borderWidth")
    }
}

class CustomCalloutView: UIView, MGLCalloutView {

    var representedObject: MGLAnnotation
    // Required views but unused for now, they can just relax
    lazy var leftAccessoryView = UIView()
    lazy var rightAccessoryView = UIView()

    weak var delegate: MGLCalloutViewDelegate?

    //MARK: Subviews -
    let titleLabel:UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = UIFont.boldSystemFont(ofSize: 17.0)
        return label
    }()

    let subtitleLabel:UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    let imageView:UIImageView = {
        let imageview = UIImageView(frame: CGRect(x: 0, y: 0, width: 25, height: 25))
        imageview.translatesAutoresizingMaskIntoConstraints = false
        imageview.contentMode = .scaleAspectFit
        return imageview
    }()

    required init(annotation: CustomAnnotation) {
        self.representedObject = annotation
        // init with 100% of width and 200px tall
        super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: UIScreen.main.bounds.width, height: 200)))

        self.titleLabel.text = self.representedObject.title ?? ""
        self.subtitleLabel.text = self.representedObject.subtitle ?? ""
        self.imageView.image = annotation.image
        setup()
    }

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

    func setup() {
        // setup this view's properties
        self.backgroundColor = UIColor.white
        // And their Subviews
        self.addSubview(titleLabel)
        self.addSubview(subtitleLabel)
        self.addSubview(imageView)
        // Add Constraints to subviews
        let spacing:CGFloat = 8.0
        imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: spacing).isActive = true
        imageView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
        imageView.heightAnchor.constraint(equalToConstant: 52.0).isActive = true
        imageView.widthAnchor.constraint(equalToConstant: 52.0).isActive = true

        titleLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: spacing).isActive = true
        titleLabel.leftAnchor.constraint(equalTo: self.imageView.rightAnchor, constant: spacing * 2).isActive = true
        titleLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -spacing).isActive = true
        titleLabel.heightAnchor.constraint(equalToConstant: 50.0).isActive = true

        subtitleLabel.topAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: spacing).isActive = true
        subtitleLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
        subtitleLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -spacing).isActive = true
        subtitleLabel.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
    }

    func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
        //Center bottom
        self.center = view.center.applying(CGAffineTransform(translationX: 0, y: view.bounds.height/2 - self.bounds.height/2))
        view.addSubview(self)
    }

    func dismissCallout(animated: Bool) {
        if (animated){
            //Implement animation here
            removeFromSuperview()
        } else {
            removeFromSuperview()
        }
    }
}