So I have a custom AnnotationView class (what the pin itself looks like) and a custom CalloutView class (what the bubble that appears above it looks like) For some reason, I get this odd behavior where if I press the annotation, it selects and appears properly (as seen in the first picture), but then if I deselect the annotation and reselect it (without moving the map at all), the callout reappears in the wrong positioning. The problem appears to be the rect.origin.y value, but I'm not sure--I tried hardcoding it but the problem still persists. In the video, I click my mouse down and it causes the annotation to move up. Any help much appreciated!
class AnnotationView: MGLAnnotationView {
weak var delegate: MapAnnotationDelegate?
var pointAnnotation: MGLPointAnnotation?
required override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
if reuseIdentifier == "default" {
super.centerOffset = CGVector(dx: 0, dy: -17.5)
}
let rect = CGRect(x: 0, y: 0, width: 35, height: 35)
let button = UIButton(frame: rect)
let image = UIImage(named: reuseIdentifier ?? "default")?.resize(targetSize: CGSize(width: 35, height: 35))
button.setImage(image, for: .normal)
button.setImage(image, for: .selected)
button.setImage(image, for: .highlighted)
button.imageView?.contentMode = .scaleAspectFit
button.addTarget(self, action: #selector(annotationAction), for: .touchUpInside)
frame = rect
addSubview(button)
isEnabled = false
}
required init?(coder: NSCoder) {
return nil
}
@objc private func annotationAction() {
delegate?.tappedAnnotation(annotation: pointAnnotation!)
}
}
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 = .white
mainBody.tintColor = .black
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()
mainBody.backgroundColor = .red
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 = .white
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()
}
}
For context, the relevant Mapbox map delegate functions are:
func tappedAnnotation(annotation: MGLPointAnnotation) {
let selectedAnnotation = control.mapView.selectedAnnotations.first
guard control.mapView.annotations != nil, selectedAnnotation !== annotation else { return }
//control is the actual map being passed into this coordinator function
for annotation in control.mapView.annotations! {
if annotation.subtitle == "PrimaryAnno" {
control.mapView.removeAnnotation(annotation)
}
}
control.mapView.selectAnnotation(annotation, animated: false, completionHandler: nil)
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if annotation is MGLPointAnnotation {
var reuseid = ""
//I use the subtitles to indicate which image I use for my annotations
if annotation.subtitle! == "primaryAnno" {
reuseid = "default"
} else {
reuseid = annotation.subtitle!
}
if let reusable = mapView.dequeueReusableAnnotationView(withIdentifier: reuseid) as? AnnotationView {
reusable.pointAnnotation = annotation as? MGLPointAnnotation
return reusable
} else {
let new = AnnotationView(reuseIdentifier: reuseid)
new.delegate = self
new.pointAnnotation = annotation as? MGLPointAnnotation
return new
}
} else {
return nil
}
}
And this is a protocol I use to define the tapping on an annotation:
protocol MapAnnotationDelegate: AnyObject {
func tappedAnnotation(annotation: MGLPointAnnotation)
}
UPDATE: I figured out that the 'correction' happens when I click on the map or change the region, so the issue is with the initial tap on annotation. I've tried setting a breakpoint in the tappedAnnotation function and it seems like once that function ends (it subsequently redirects to the annotationAction() function), the callout just renders in the wrong position.

