Related
I'm trying to update some map components while dragging an annotations like highlighting a specific MGLPolygon and panning the map if the annotation is already dragged near the edge. I will use the later for this problem.
I tried the code https://docs.mapbox.com/ios/maps/examples/draggable-views/ and added some lines. Here's the exact copy with my changes.
import Mapbox
// Example view controller
class ViewController: UIViewController, MGLMapViewDelegate {
var mapView: MGLMapView!
override func viewDidLoad() {
super.viewDidLoad()
mapView = MGLMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.styleURL = MGLStyle.streetsStyleURL
mapView.tintColor = .darkGray
mapView.zoomLevel = 1
mapView.delegate = self
view.addSubview(mapView)
// Specify coordinates for our annotations.
let coordinates = [
CLLocationCoordinate2D(latitude: 0, longitude: -70),
CLLocationCoordinate2D(latitude: 0, longitude: -35),
CLLocationCoordinate2D(latitude: 0, longitude: 0),
CLLocationCoordinate2D(latitude: 0, longitude: 35),
CLLocationCoordinate2D(latitude: 0, longitude: 70)
]
// Fill an array with point annotations and add it to the map.
var pointAnnotations = [MGLPointAnnotation]()
for coordinate in coordinates {
let point = MGLPointAnnotation()
point.coordinate = coordinate
point.title = "To drag this annotation, first tap and hold."
pointAnnotations.append(point)
}
mapView.addAnnotations(pointAnnotations)
}
// MARK: - MGLMapViewDelegate methods
// 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 MGLPointAnnotation else {
return nil
}
// For better performance, always try to reuse existing annotations. To use multiple different annotation views, change the reuse identifier for each.
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "draggablePoint") {
return annotationView
} else {
let dav = DraggableAnnotationView(reuseIdentifier: "draggablePoint", size: 50)
dav.mapView = mapView
return dav
}
}
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}
}
// MGLAnnotationView subclass
class DraggableAnnotationView: MGLAnnotationView {
var mapView: MGLMapView!
var screen: CGRect!
var mapBounds: CGRect!
init(reuseIdentifier: String, size: CGFloat) {
super.init(reuseIdentifier: reuseIdentifier)
// `isDraggable` is a property of MGLAnnotationView, disabled by default.
isDraggable = true
// This property prevents the annotation from changing size when the map is tilted.
scalesWithViewingDistance = false
// Begin setting up the view.
frame = CGRect(x: 0, y: 0, width: size, height: size)
backgroundColor = .darkGray
// Use CALayer’s corner radius to turn this view into a circle.
layer.cornerRadius = size / 2
layer.borderWidth = 1
layer.borderColor = UIColor.white.cgColor
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.1
screen = UIScreen.main.bounds
mapBounds = CGRect(
x: screen.origin.x + 20,
y: screen.origin.y + 20,
width: screen.size.width - 40,
height: screen.size.height - 40)
}
// These two initializers are forced upon us by Swift.
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Custom handler for changes in the annotation’s drag state.
override func setDragState(_ dragState: MGLAnnotationViewDragState, animated: Bool) {
super.setDragState(dragState, animated: animated)
switch dragState {
case .starting:
print("Starting", terminator: "")
startDragging()
case .dragging:
let pointCoordinate = self.mapView.convert(center, toCoordinateFrom: nil)
if mapBounds.contains(center) {
DispatchQueue.main.async {
self.mapView.setCenter(pointCoordinate, animated: true)
}
}
print(".", terminator: "")
case .ending, .canceling:
print("Ending")
endDragging()
case .none:
break
#unknown default:
fatalError("Unknown drag state")
}
}
// When the user interacts with an annotation, animate opacity and scale changes.
func startDragging() {
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: [], animations: {
self.layer.opacity = 0.8
self.transform = CGAffineTransform.identity.scaledBy(x: 1.5, y: 1.5)
}, completion: nil)
// Initialize haptic feedback generator and give the user a light thud.
if #available(iOS 10.0, *) {
let hapticFeedback = UIImpactFeedbackGenerator(style: .light)
hapticFeedback.impactOccurred()
}
}
func endDragging() {
transform = CGAffineTransform.identity.scaledBy(x: 1.5, y: 1.5)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: [], animations: {
self.layer.opacity = 1
self.transform = CGAffineTransform.identity.scaledBy(x: 1, y: 1)
}, completion: nil)
// Give the user more haptic feedback when they drop the annotation.
if #available(iOS 10.0, *) {
let hapticFeedback = UIImpactFeedbackGenerator(style: .light)
hapticFeedback.impactOccurred()
}
}
}
Everytime the self.mapView.setCenter(pointCoordinate, animated: true) gets called, the annotations goes back and forth to its original position.
Here is the code explaining the Adonis's solution. Essentially add a pan gesture to a custom annotation's view and update the coords as and when the annotation is panned.
class CustomDraggableAnnotaionView: MGLAnnotationView {
required init(
reuseIdentifier: String?,
image: UIImage?,
annotation: CustomMapGLAnnotaion
) {
super.init(reuseIdentifier: reuseIdentifier)
setupDraggableAnnotations()
self.layer.zPosition = 10
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Draggable annotation handlers
private func setupDraggableAnnotations() {
addDraggableAnnotationGestureRecognizers()
}
private func addDraggableAnnotationGestureRecognizers() {
let panGesture = UIPanGestureRecognizer(
target: self,
action: #selector(self.draggedView(_:))
)
let tapGesture = UITapGestureRecognizer(
target: self,
action: #selector(self.tappedAnnotation(_:))
)
self.isUserInteractionEnabled = true
self.addGestureRecognizer(panGesture)
self.addGestureRecognizer(tapGesture)
for recognizer in self.gestureRecognizers! where recognizer is UITapGestureRecognizer {
tapGesture.require(toFail: recognizer)
}
for recognizer in self.gestureRecognizers! where recognizer is UIPanGestureRecognizer {
panGesture.require(toFail: recognizer)
}
}
#objc func draggedView(_ sender: UIPanGestureRecognizer) {
annotationObject?.draggable!.isCurrentlyDragging = true
let point = sender.location(in: MapManager.shared.mapView)
let coordinates = MapManager.shared.mapView.convert(
point,
toCoordinateFrom: MapManager.shared.mapView
)
annotationObject?.coordinate = coordinates
if sender.state == .ended {
// endDragging()
} else if sender.state == .began {
// startDragging()
annotationObject?.draggable!.handler.didStartDragging()
} else {
//
}
}
}
Right now my app display a customUserAnnotationView with a custom image where the user annotation is (you can see this in ViewController.swift). I have also created a custom UIView that I want to use as an annotation just above the user annotation (the code and image for it are under SpeechBubble.swift).
I want to combine these two objects so that I can show the CustomUserAnnotationView with the Custom UIView(SpeechBubble.swift) placed in an annotation above.
My attempts at making a frankenstein program from multiple mapbox tutorials have not worked out for me. I only want to place the custom annotation class I created above the image, and maybe add a small triangle to make it look like a speech bubble.
ViewController.swift
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let mapView = MGLMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.delegate = self
// Enable heading tracking mode so that the arrow will appear.
mapView.userTrackingMode = .followWithHeading
// Enable the permanent heading indicator, which will appear when the tracking mode is not `.followWithHeading`.
mapView.showsUserHeadingIndicator = true
view.addSubview(mapView)
let idea = UITextView(frame: CGRect(x: 0, y: 0, width: 100, height: 40))
idea.text = "Hello There"
idea.textAlignment = NSTextAlignment.center
let sb = SpeechBubble(coord: mapView.targetCoordinate, idea: idea)
mapView.addSubview(sb)
}
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
// Substitute our custom view for the user location annotation. This custom view is defined below.
if annotation is MGLUserLocation && mapView.userLocation != nil {
return Avatar()
}
return nil
}
// Optional: tap the user location annotation to toggle heading tracking mode.
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
if mapView.userTrackingMode != .followWithHeading {
mapView.userTrackingMode = .followWithHeading
} else {
mapView.resetNorth()
}
// We're borrowing this method as a gesture recognizer, so reset selection state.
mapView.deselectAnnotation(annotation, animated: false)
}
}
SpeechBubble.swift
import UIKit
import Mapbox
class SpeechBubble: UIView, MGLMapViewDelegate{
//var sbView: UIView
init(coord: CLLocationCoordinate2D, idea: UITextView) {
let width = CGFloat(180)
let height = UITextField.layoutFittingExpandedSize.height + 32
super.init(frame: CGRect(x: CGFloat(coord.latitude), y: CGFloat(coord.longitude), width: width, height: height))
self.addSubview(idea)
self.addSubview(buttonsView());
self.addSubview(upvoteView());
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func upvoteView() -> UIView {
let uView = UIView()
let vCnt = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 21))
vCnt.center = CGPoint(x: 10.5, y: 32)
vCnt.textAlignment = .center
vCnt.text = "0"
let uButton = UIButton(type: .custom)
uButton.frame = CGRect(x: vCnt.frame.size.width + 5, y: 0, width: 32, height: 32);
let uImage = UIImage (named: "Upvote")
uButton.setImage(uImage, for: .normal)
uView.frame.size.width = vCnt.frame.size.width + uButton.frame.size.width + 5
uView.frame.size.height = max(vCnt.frame.size.height, uButton.frame.size.height)
uView.frame = CGRect(
x: 0,
y: self.frame.size.height - uView.frame.size.height,
width: uView.frame.size.width,
height: uView.frame.size.height );
uView.addSubview(vCnt)
uView.addSubview(uButton)
return uView
}
func buttonsView() -> UIView {
let bView = UIView()
let jButton = UIButton(type: .custom)
rButton.frame = CGRect(x: 0, y: 0, width: 35, height: 32);
let rImage = UIImage (named: "Rocket")
rButton.setImage(rImage, for: .normal)
let pButton = UIButton(type: .custom)
pButton.frame = CGRect(x: jButton.frame.size.width + 5, y: 0, width: 31, height: 36);
let pImage = UIImage (named: "Profile")
pButton.setImage(pImage, for: .normal)
bView.frame.size.width = rButton.frame.size.width + pButton.frame.size.width + 5
bView.frame.size.height = max(rButton.frame.size.height, pButton.frame.size.height)
bView.frame = CGRect(
x: self.frame.size.width - bView.frame.size.width,
y: self.frame.size.height - bView.frame.size.height,
width: bView.frame.size.width,
height: bView.frame.size.height );
bView.addSubview(rButton)
bView.addSubview(pButton)
return bView
}
}
Avatar.swift
import Mapbox
class Avatar: MGLUserLocationAnnotationView {
let size: CGFloat = 48
var arrow: CALayer!
//var arrow: CAShapeLayer!
// -update is a method inherited from MGLUserLocationAnnotationView. It updates the appearance of the user location annotation when needed. This can be called many times a second, so be careful to keep it lightweight.
override func update() {
if frame.isNull {
frame = CGRect(x: 0, y: 0, width: size, height: size)
return setNeedsLayout()
}
// Check whether we have the user’s location yet.
if CLLocationCoordinate2DIsValid(userLocation!.coordinate) {
setupLayers()
updateHeading()
}
}
private func updateHeading() {
// Show the heading arrow, if the heading of the user is available.
if let heading = userLocation!.heading?.trueHeading {
arrow.isHidden = false
// Get the difference between the map’s current direction and the user’s heading, then convert it from degrees to radians.
let rotation: CGFloat = -MGLRadiansFromDegrees(mapView!.direction - heading)
// If the difference would be perceptible, rotate the arrow.
if abs(rotation) > 0.01 {
// Disable implicit animations of this rotation, which reduces lag between changes.
CATransaction.begin()
CATransaction.setDisableActions(true)
arrow.setAffineTransform(CGAffineTransform.identity.rotated(by: rotation))
CATransaction.commit()
}
} else {
arrow.isHidden = true
}
}
private func setupLayers() {
// This dot forms the base of the annotation.
if arrow == nil {
arrow = CALayer()
let myImage = UIImage(named: "will_smith")?.cgImage
arrow.bounds = CGRect(x: 0, y: 0, width: size, height: size)
arrow.contents = myImage
layer.addSublayer(arrow)
}
}
// Calculate the vector path for an arrow, for use in a shape layer.
private func arrowPath() -> CGPath {
let max: CGFloat = size / 2
let pad: CGFloat = 3
let top = CGPoint(x: max * 0.5, y: 0)
let left = CGPoint(x: 0 + pad, y: max - pad)
let right = CGPoint(x: max - pad, y: max - pad)
let center = CGPoint(x: max * 0.5, y: max * 0.6)
let bezierPath = UIBezierPath()
bezierPath.move(to: top)
bezierPath.addLine(to: left)
bezierPath.addLine(to: center)
bezierPath.addLine(to: right)
bezierPath.addLine(to: top)
bezierPath.close()
return bezierPath.cgPath
}
}
--------------------------------------------------------------------------------------------------------
UPDATE
I tried to create a Frankenstein program of the Answer and my code and am receiving the following the error Property 'self.representedObject' not initialized at super.init call within SpeechBubble.swift. I also move all my old code from speechBubble.swift into insideSpeechBubble.swift
Updated SpeechBubble.swift
import UIKit
import Mapbox
class SpeechBubble: UIView, MGLCalloutView {
// Your IBOutlets //
var representedObject: MGLAnnotation
var annotationPoint: CGPoint
// Required views but unused for this implementation.
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()
var contentView: MGLMapView
weak var delegate: MGLCalloutViewDelegate?
// MARK: - init methods
required init(annotation: MGLAnnotation, frame: CGRect, annotationPoint: CGPoint) {
let idea = UITextView(frame: CGRect(x: 0, y: 0, width: 100, height: 40))
idea.text = "Hello There"
idea.textAlignment = NSTextAlignment.center
self.representedObject = annotation
self.annotationPoint = annotationPoint
contentView = InsideSpeechBubble(coord: annotationPoint, idea: idea )
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("SpeechBubble", owner: self, options: nil)
addSubview(contentView as UIView)
contentView.frame = self.bounds
// Do your initialisation //
}
// MARK: - MGLCalloutView methods
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
// Present the custom callout slightly above the annotation's view. Initially invisble.
self.center = annotationPoint.applying(CGAffineTransform(translationX: 0, y: -self.frame.height - 20.0))
// I have logic here for setting the correct image and button states //
}
func dismissCallout(animated: Bool) {
removeFromSuperview()
}
}
Updated ViewController.swift
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
//let point = MGLPointAnnotation()
override func viewDidLoad() {
super.viewDidLoad()
let mapView = MGLMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.delegate = self
// Enable heading tracking mode so that the arrow will appear.
mapView.userTrackingMode = .followWithHeading
// Enable the permanent heading indicator, which will appear when the tracking mode is not `.followWithHeading`.
mapView.showsUserHeadingIndicator = true
view.addSubview(mapView)
let HighDea = UITextView(frame: CGRect(x: 0, y: 0, width: 100, height: 40))
HighDea.text = "Hello There"
HighDea.textAlignment = NSTextAlignment.center
//let sb = SpeechBubble()
//mapView.addSubview(sb)
}
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
// Substitute our custom view for the user location annotation. This custom view is defined below.
if annotation is MGLUserLocation && mapView.userLocation != nil {
return Avatar()
}
return nil
}
func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
// Do your annotation-specific preparation here //
// I get the correct size from my xib file.
let viewFrame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 261.0, height: 168.0))
// Get the annotation's location in the view's coordinate system.
let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)
let customCalloutView = SpeechBubble(annotation: annotation, frame: viewFrame, annotationPoint: annotationPoint)
return customCalloutView
}
// func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
// This example is only concerned with point annotations.
// guard annotation is MGLPointAnnotation 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(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
// }
// Optional: tap the user location annotation to toggle heading tracking mode.
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
if mapView.userTrackingMode != .followWithHeading {
mapView.userTrackingMode = .followWithHeading
} else {
mapView.resetNorth()
}
// We're borrowing this method as a gesture recognizer, so reset selection state.
mapView.deselectAnnotation(annotation, animated: false)
}
}
When I implemented a custom callout for my Mapbox annotations I used a xib file to design the actual callout. I find that it gives me a lot more instant feedback than than trying to conjure the UI from code (but obviously do whatever your preference is).
Which gives me something like the following.
Using a UIImage for the background allows me to achieve any shape I choose. Here I use transparency around the white to give me the circular elements and the bottom triangle you mention in your question.
The Swift file for this UIView (your SpeechBubble) needs to conform to the MGLCalloutView protocol not MGLMapViewDelegate as you have it currently. Your ViewController is the MGLMapViewDelegate, not your custom callout. Pair the xib file and the Swift file in the usual way in Identity Inspector in IB. So would be something like this:
import UIKit
import Mapbox
class SpeechBubble: UIView, MGLCalloutView {
// Your IBOutlets //
#IBOutlet var contentView: UIView! // The custom callout's view.
var representedObject: MGLAnnotation
var annotationPoint: CGPoint
// Required views but unused for this implementation.
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()
weak var delegate: MGLCalloutViewDelegate?
// MARK: - init methods
required init(annotation: YourAnnotation, frame: CGRect, annotationPoint: CGPoint) {
self.representedObject = annotation
self.annotationPoint = annotationPoint
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("SpeechBubble", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds
// Do your initialisation //
}
// MARK: - MGLCalloutView methods
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
// Present the custom callout slightly above the annotation's view. Initially invisble.
self.center = annotationPoint.applying(CGAffineTransform(translationX: 0, y: -self.frame.height - 20.0))
// I have logic here for setting the correct image and button states //
}
func dismissCallout(animated: Bool) {
removeFromSuperview()
}
Then you just seem to be missing the MGLMapViewDelegate method to actually return your SpeechBubble view when requested. It should be in your ViewController file.
func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
// Do your annotation-specific preparation here //
// I get the correct size from my xib file.
let viewFrame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 261.0, height: 168.0))
// Get the annotation's location in the view's coordinate system.
let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)
let customCalloutView = SpeechBubble(annotation: YourAnnotation, frame: viewFrame, annotationPoint: annotationPoint)
return customCalloutView
}
Hopefully this will get you closer to achieving what you're trying to do. BTW this version of your question is miles ahead of the first one.
EDIT +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
It's going to be almost impossible to work this through without sight of your project so I have put together a bare bones implementation. It is based on the Mapbox example here: Mapbox Custom Callout which for some reason doesn't show how to actually supply the callout view. I've also extended it to allow for a custom annotation image. If you can get this working you should be able to move the relevant parts into your own project.
I strongly recommend that if you try to implement the stuff below that you do it in a fresh project.
The view controller.
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 coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)
let marker = MyAnnotation(coordinate: coordinate, title: "Bingo", subtitle: "Bongo")
// Add marker to the map.
mapView.addAnnotation(marker)
}
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.
let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)
let viewFrame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 250.0, height: 178.0))
return CustomCalloutView(representedObject: annotation, frame: viewFrame, annotationPoint: annotationPoint)
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "myAnnotationView") {
return annotationView
} else {
let annotationView = MyAnnotationView(reuseIdentifier: "myAnnotationView", size: CGSize(width: 45, height: 45), annotation: annotation)
return annotationView
}
}
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)
}
}
CustomCalloutView.swift
import UIKit
import Mapbox
class CustomCalloutView: UIView, MGLCalloutView {
#IBOutlet var contentView: UIView!
weak var delegate: MGLCalloutViewDelegate?
var representedObject: MGLAnnotation
var annotationPoint: CGPoint
// Required views but unused for this implementation.
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()
required init(representedObject: MGLAnnotation, frame: CGRect, annotationPoint: CGPoint) {
self.representedObject = representedObject
self.annotationPoint = annotationPoint
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
let coordinate = CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)
self.representedObject = MyAnnotation(coordinate: coordinate, title: "", subtitle: "")
self.annotationPoint = CGPoint(x: 50.0, y: 50.0)
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil)
addSubview(contentView)
}
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
// Present the custom callout slightly above the annotation's view. Initially invisble.
self.center = annotationPoint.applying(CGAffineTransform(translationX: 0.0, y: -120.0))
view.addSubview(self)
}
func dismissCallout(animated: Bool) {
removeFromSuperview()
}
}
This is associated/identified with a xib file. It just contains a simple image shape for now. I had to (re)introduce the contentView IBOutlet as I was having trouble loading things from the Bundle and adding it to self in commonInit() made everything happy.
The custom annotation class.
import UIKit
import Mapbox
// MGLAnnotation protocol reimplementation
class MyAnnotation: NSObject, MGLAnnotation {
// As a reimplementation of the MGLAnnotation protocol, we have to add mutable coordinate and (sub)title properties ourselves.
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
// Custom properties that we will use to customize the annotation.
var image: UIImage?
var reuseIdentifier: String?
init(coordinate: CLLocationCoordinate2D, title: String?, subtitle: String?) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
self.reuseIdentifier = "myAnnotation"
}
}
The MGLAnnotationView subclass.
import UIKit
import Mapbox
class MyAnnotationView: MGLAnnotationView {
init(reuseIdentifier: String, size: CGSize, annotation: MGLAnnotation) {
super.init(reuseIdentifier: reuseIdentifier)
// This property prevents the annotation from changing size when the map is tilted.
scalesWithViewingDistance = false
// Begin setting up the view.
frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
let imageView = UIImageView(frame: frame)
var image = UIImage()
if annotation is MyAnnotation {
image = UIImage(named: "frog")!
}
imageView.image = image
addSubview(imageView)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Naturally there is a lot of hard coded numbers and the requirement for an image called frog but you can change all of that and improve it as you wish. The CustomCalloutView.swift and CustomCalloutView.xib need to be linked in the usual way in the identity inspector, etc.
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.
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)
}
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()
}
}
}
In my app, the user saves some data, including a map coordinate. In my code, a pin is dropped at the saved map coordinate. Here is my code-
override func viewDidLoad() {
super.viewDidLoad()
self.mapView.delegate = self
// Data loading
itemNameTextField.delegate = self
itemDescriptionLabel.delegate = self
itemLocationTextView.delegate = self
// Press recognizer
if let item = item {
itemNameTextField.text = item.itemName
itemDescriptionLabel.text = item.itemDescription
itemLocationTextView.text = item.itemPlace
let dropPin = MKPointAnnotation()
dropPin.coordinate = mapView.convert(item.mapPoint, toCoordinateFrom: mapView)
dropPin.title = "Location of \(item.itemName)"
self.mapView.addAnnotation(dropPin)
print("Set the location of item pin to \(String(describing: dropPin.coordinate))")
}
// Styles
itemInfoView.layer.cornerRadius = 3
itemInfoView.layer.shadowColor = UIColor(red:0/255.0, green:0/255.0, blue:0/255.0, alpha: 1.0).cgColor
itemInfoView.layer.shadowOffset = CGSize(width: 0, height: 1.75)
itemInfoView.layer.shadowRadius = 1.7
itemInfoView.layer.shadowOpacity = 0.45
itemLocationView.layer.cornerRadius = 3
itemLocationView.layer.shadowColor = UIColor(red:0/255.0, green:0/255.0, blue:0/255.0, alpha: 1.0).cgColor
itemLocationView.layer.shadowOffset = CGSize(width: 0, height: 1.75)
itemLocationView.layer.shadowRadius = 1.7
itemLocationView.layer.shadowOpacity = 0.45
locationAdressTextview.layer.cornerRadius = 2
locationAdressTextview.layer.shadowColor = UIColor(red:0/255.0, green:0/255.0, blue:0/255.0, alpha: 1.0).cgColor
locationAdressTextview.layer.shadowOffset = CGSize(width: 0, height: 1.50)
locationAdressTextview.layer.shadowRadius = 1.6
locationAdressTextview.layer.shadowOpacity = 0.3
}
I know that the app does save the pin coordinates as a CGPoint, and I know that it converts it from a CGPoint to a CLLocationCoordinate2D, because of the print statements I placed. However, when the screen loads, the print statement shows a valid coordinate, but there is no pin on the map, and I get no errors. Can somebody please help me? Thanks!
Whenever the map should display an annotation the MKMapViewDelegate method viewForAnnotation is called so you have to implement that method and return a view according to your needs
Sample code:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "myAnnotationView")
// configure the view
return annotationView
}
I am trying to customise the MKAnnotationView for my mapView callout bubbles. I am fine with setting the annotation title when the annotation is created, and also customising the MKAnnotationView to add labels or images e.t.c (in the viewForAnnotation delegate), but how do I change the label created in the viewForAnnotation delegate, so that the title of it is different for each pin?
The other issue I have is that if I don't add a title or subtitle to the annotation when it is created in the viewDidLoad method, but I still try and create one by leaving self.map.addAnnotation(annotation), when I run the app and tap the pin no callout bubble is displayed.
In the end I would like to have totally customised callout bubbles, with individual labels on them for each pin. So what i really ned to know is how to access the viewForAnnotation delegate when the annotation is created to change properties of it for each pin.
override func viewDidLoad() {
super.viewDidLoad()
var countries: [String] = ["Germany","Germany","Poland","Denmark"]
var practiceRoute: [CLLocationCoordinate2D] = [CLLocationCoordinate2DMake(50, 10),CLLocationCoordinate2DMake(52, 9),CLLocationCoordinate2DMake(53, 20),CLLocationCoordinate2DMake(56, 14)]
for vari=0; i<practiceRoute.count; i++ {
var annotation = MKPointAnnotation
annotation.title = countries[i]
annotation.coordinate = practiceRoute[i]
self.map.addAnnotation(annotation)
}
}
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if annotation is MKUserLocation {
return nil
}
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView
if(pinView==nil){
pinView=MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
pinView!.canShowCallout = true
let base = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
base.backgroundColor = UIColor.lightGrayColor()
let label1 = UILabel(frame: CGRect(x: 30, y: 10, width: 60, height: 15))
label1.textColor = UIColor.blackColor()
label1.text = "12 photos"
base.addSubview(label1)
pinView!.leftCalloutAccessoryView = base
pinView!.pinColor = .Red
}
return pinView
}
Make your custom annotation view
There is no public API allowing you to access the label in the pop up directly. What you need to do is make a subclass of MKPinAnnotationView and do whatever customization you want there. As an example,
class CustomAnnotationView : MKPinAnnotationView
{
let selectedLabel:UILabel = UILabel.init(frame:CGRectMake(0, 0, 140, 38))
override func setSelected(selected: Bool, animated: Bool)
{
super.setSelected(false, animated: animated)
if(selected)
{
// Do customization, for example:
selectedLabel.text = "Hello World!!"
selectedLabel.textAlignment = .Center
selectedLabel.font = UIFont.init(name: "HelveticaBold", size: 15)
selectedLabel.backgroundColor = UIColor.lightGrayColor()
selectedLabel.layer.borderColor = UIColor.darkGrayColor().CGColor
selectedLabel.layer.borderWidth = 2
selectedLabel.layer.cornerRadius = 5
selectedLabel.layer.masksToBounds = true
selectedLabel.center.x = 0.5 * self.frame.size.width;
selectedLabel.center.y = -0.5 * selectedLabel.frame.height;
self.addSubview(selectedLabel)
}
else
{
selectedLabel.removeFromSuperview()
}
}
}
Other Notes
Use this custom view in the map view:
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
var anno = mapView.dequeueReusableAnnotationViewWithIdentifier("Anno")
if anno == nil
{
anno = CustomAnnotationView.init(annotation: annotation, reuseIdentifier: "Anno")
}
return anno;
}
Since the title property of the annotation is not set, you will have to call the map view function selectAnnotation yourself. Add the following to the CustomAnnotationView class:
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
mapView?.selectAnnotation(self.annotation!, animated: true)
}
If you want to have more than one marker on the map:
Usually just draw the annotation simply during initialization. In setSelected just return false (meaning "show all annotations all the time").
class DotAnnotationView : MKPinAnnotationView {
let dot: UILabel = UILabel.init(frame:CGRect(x: 0, y: 0, width: 20, height: 20))
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
_setup()
}
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
_setup()
}
override func prepareForReuse() {
dot.text = "you forgot to set the text value?"
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(false, animated: animated)
}
func _setup() {
dot.textAlignment = .center
.. etc
}
}
You set the string (or other values - say color of the panel) for each annotation in mapView#viewFor. It's like populating a cell in a UITableView.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let textForThisItem = annotation.title!!
// or, just use index#of to determine which row this is in your data array
if annotation.isEqual(mkMap.userLocation) {
// skip the user-position indicator
return nil
}
var anno = mapView.dequeueReusableAnnotationView(withIdentifier: "anno")
if anno == nil {
anno = DotAnnotationView.init(annotation: annotation, reuseIdentifier: "anno")
}
(anno as! DotAnnotationView).dot.text = textForThisItem
return anno
}
Finally note that somewhat confusingly, if you very simply change the class of CustomAnnotationView from MKPinAnnotationView to MKAnnotationView, everything works the same but it replaces "all of the pin" rather than just the annotation.
Updated the code for the latest swift.
This is the new subclass of MKPinAnnotationView which you can copy paste to test:
import UIKit
import MapKit
class CustomAnnotationView : MKPinAnnotationView
{
let selectedLabel:UILabel = UILabel.init(frame:CGRect(x:0, y:0, width:140, height:38))
override func setSelected(_ selected: Bool, animated: Bool)
{
super.setSelected(false, animated: animated)
if(selected)
{
// Do customization, for example:
selectedLabel.text = "Hello World!!"
selectedLabel.textAlignment = .center
selectedLabel.font = UIFont.init(name: "HelveticaBold", size: 15)
selectedLabel.backgroundColor = UIColor.gray
selectedLabel.layer.borderColor = UIColor.darkGray.cgColor
selectedLabel.layer.borderWidth = 2
selectedLabel.layer.cornerRadius = 5
selectedLabel.layer.masksToBounds = true
selectedLabel.center.x = 0.5 * self.frame.size.width;
selectedLabel.center.y = -0.5 * selectedLabel.frame.height;
self.addSubview(selectedLabel)
}
else
{
selectedLabel.removeFromSuperview()
}
}
}