paul_f
paul_f

Reputation: 1375

How do you create a Custom Callout for Mapbox annotations?

I've tried for a few hours. The materials on the Mapbox Website just shows this:

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

The problems are that there is no elaboration on what a 'CustomCalloutView' is or contains to achieve a CustomCallout. I understand (I think) its a class that implements MGLCalloutView but creating a class that correctly implements that method is no easy task, I am getting all sorts of errors particularly around one function 'self' -> Self.

It would be great to see an example of how to actually implement a Custom Callout. All of the conversations on Mapbox Git is just too complicated for a simpleton like me.

Upvotes: 3

Views: 3108

Answers (1)

RLoniello
RLoniello

Reputation: 2329

MGLAnnotation is a NSObjectProtocol, That only requires the classes and/or object that implements it to have a CLLocationCoordinate2D. This object should be your data model or relate very closely to it. For simplicity I inherited from NSObject.

CustomAnnotation.swift

import Foundation
import UIKit
import Mapbox

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

Your custom callout view (MGLCalloutView) is yet another protocol that any class or object inheriting from NSObject can conform to and has the following required properties, note that I am subclassing with UIView which inherits from NSObject:

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

    var delegate: MGLCalloutViewDelegate?

    required init(annotation: MGLAnnotation) {
        self.representedObject = annotation
        super.init()
    }

    func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {

    }

    func dismissCallout(animated: Bool) {

    }
}

Note that the require init(annotation:) is a bit misleading as one would expect annotation to be an object, instead it is an object that conforms to MGLAnnotation, so we can change this to our own data model version of MGLAnnotation.

required init(annotation: CustomAnnotation) {
    self.representedObject = annotation
    super.init()
}

Now, in the MGLCalloutViewDelegate delegate method presentCallout(rect:view:constrainedRect:) we add the custom callout (self) to the mapView which is passed into the delegate function as view. We also want to remove the view from the super view when it is dismissed:

func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
    view.addSubview(self)

}

func dismissCallout(animated: Bool) {
    if (animated){
        //do something cool
        removeFromSuperview()
    } else {
        removeFromSuperview()
    }
}

Finally in your mapView(_: calloutViewFor annotation:) method create a new custom annotation from your class or object that conformed to MGLAnnotation and pass it to your custom callout view:

func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {

    let title = annotation.title ?? nil
    let subtitle = annotation.subtitle ?? nil
    let image = UIImage(named: "apple.png")!
    let customAnnotation = CustomAnnotation(coordinate: annotation.coordinate, title: title ?? "no title", subtitle: subtitle ?? "no subtitle", image: image)

    return CustomCalloutView(annotation: customAnnotation)
}

View Hierarchy

For reference here is the rest of my full implementation:

CustomAnnotation.swift

see above

ViewController.swift

import UIKit
import Mapbox

class ViewController: UIViewController, MGLMapViewDelegate {

    lazy var mapView: MGLMapView = {
        let mv = MGLMapView(frame: self.view.bounds, styleURL: URL(string: "mapbox://styles/mapbox/streets-v10"))
        mv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mv.setCenter(CLLocationCoordinate2D(latitude: 40.7326808, longitude: -73.9843407), zoomLevel: 9, animated: false)
        return mv
    }()


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        setup()

        // Declare the marker `hello` and set its coordinates, title, and subtitle.
        let hello = MGLPointAnnotation()
        hello.coordinate = CLLocationCoordinate2D(latitude: 40.7326808, longitude: -73.9843407)
        hello.title = "Hello world!"
        hello.subtitle = "Welcome to my marker"

        // Add marker `hello` to the map.
        mapView.addAnnotation(hello)
    }

    func setup() {
        self.view.addSubview(mapView)
        mapView.delegate = self
    }


    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    // Use the default marker. See also: our view annotation or custom marker examples.
    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        return nil
    }

    // Allow callout view to appear when an annotation is tapped.
    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return true
    }


    func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {

        let title = annotation.title ?? nil
        let subtitle = annotation.subtitle ?? nil
        let image = UIImage(named: "apple.png")!
        let customAnnotation = CustomAnnotation(coordinate: annotation.coordinate, title: title ?? "no title", subtitle: subtitle ?? "no subtitle", image: image)

        return CustomCalloutView(annotation: customAnnotation)
    }
}

CustomCalloutView

import Foundation
import Mapbox

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 75% of width and 120px tall
        super.init(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: UIScreen.main.bounds.width * 0.75, height: 120.0)))

        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) {
        //Always, Slightly above center
        self.center = view.center.applying(CGAffineTransform(translationX: 0, y: -self.frame.height))
        view.addSubview(self)

    }

    func dismissCallout(animated: Bool) {
        if (animated){
            //do something cool
            removeFromSuperview()
        } else {
            removeFromSuperview()
        }

    }
}

Upvotes: 10

Related Questions