how to animate drop of MKAnnotationView with custom image - ios

when I try to access the animate drop property of an MKAnnotationView, it doesn't exist. I know this property exists for a MKPinAnnotationView, but I'm using a custom image and therefore have to use a MKAnnotationView. Any way to animate the drop of a MKAnnotationView? Code:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: pinIdentifier)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: pinIdentifier)
annotationView?.canShowCallout = true
let pinImage = UIImage(named:"mappin")!
annotationView?.image = pinImage
//annotationView?.animatesDrop = true
} else {
annotationView?.annotation = annotation
}
return annotationView
}

1) Create a xib file and an associated .swift file to hold your AnimatedAnnotation code.
2) in AnimatedAnnotation.xib you can put your UI creativity to task and create your annotation.
3) In your AnimatedAnnotation.swift file, create your own AnimatedAnnotation class.
class AnimatedAnnotation: MKAnnotationView {
}
4) Next, write a configuration method that will configure an instance of this "invisible" container view with an animationDuration, and annotationImage (this will be your custom image).
class AnimatedAnnotation: UIView {
var animationDuration: Double!
var annotationImage: UIImage!
func configureWith(animationDuration: Double, annotationImage: UIImage) {
self.backgroundColor = .clear
self.annotationImage = annotationImage
self.animationDuration = animationDuration
}
}
5) Then declare a property animatedView of type UIImageView and instantiate it within your configuration block.
class AnimatedAnnotation: UIView {
var animatedView: UIImageView!
var animationDuration: Double!
var annotationImage: UIImage!
func configureWith(animationDuration: Double, annotationImage: UIImage) {
self.backgroundColor = .clear
self.annotationImage = annotationImage
self.animationDuration = animationDuration
instantiateAnimatedView()
}
func instantiateAnimatedView() {
let startingPosition = CGRect(x: 0, y: 0, width: 20, height: 20) // This is whatever starting position you want
let imageView = UIImageView(frame: startingPosition)
imageView.image = UIImage(name: "YourAnnotationImage")
imageView.alpha = 0
addSubview(imageView!)
}
}
6) Write the method to make the animated view appear and animate on top of it's "invisible" superview.
class AnimatedAnnotation: UIView {
var animatedView: UIImageView!
var animationDuration: Double!
var annotationImage: UIImage!
func configureWith(animationDuration: Double, annotationImage: UIImage) {
self.backgroundColor = .clear
self.annotationImage = annotationImage
self.animationDuration = animationDuration
instantiateAnimatedView()
dropAnnotation() // Call whenever you want the drop to happen
}
func instantiateAnimatedView() {
let startingPosition = CGRect(x: 0, y: 0, width: 20, height: 20) // This is whatever starting position you want
let imageView = UIImageView(frame: startingPosition)
imageView.image = annotationImage
imageView.alpha = 0
addSubview(imageView!)
}
func dropAnnotation() {
let dropHeight:CGFloat = self.bounds.size.height - animatedView.frame.size.height
let endPosition = CGPoint(x: animatedView.center.x, y: animatedView.center.y + dropHeight)
UIView.animate(withDuration: animationDuration, animations: {
self.animatedView.alpha = 1
self.animatedView.center = endPosition
})
}
}
7) USAGE in your viewFor annotation: MKAnnotation) -> MKAnnotationView? method, instantiate an AnimatedAnnotation and return that in the method.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let bundle = Bundle.main // or your framework bundle
let xib = UINib(nibName: "AnimatedAnnotation", bundle: bundle)
let view = xib.instantiate(withOwner: self, options: nil)[0] as! AnimatedAnnotation
view.configureWith(animationDuration: 0.25, annotationImage: UIImage(named: "Your-Annotation-Image")
return view
}
I haven't actually tried this in Xcode, and I just wrote this in the SO editor, but this should work. Let me know your progress.

Related

Name under custom annotation views

I have a custom annotation view, when I click on any annotation point, I can see the custom view with all information. but what I need is to see name of each industrial parks under each annotation points. now I can see only point but without names
I need to see name under points.
//MARK: MKMapViewDelegate
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation
{
return nil
}
var annotationView = self.mapView.dequeueReusableAnnotationView(withIdentifier: "Pin")
if annotationView == nil{
annotationView = AnnotationView(annotation: annotation, reuseIdentifier: "Pin")
annotationView?.canShowCallout = false
}else{
annotationView?.annotation = annotation
}
annotationView?.image = UIImage(named: "test3a")
return annotationView
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)
{
// 1
if view.annotation is MKUserLocation
{
// Don't proceed with custom callout
return
}
// 2
let starbucksAnnotation = view.annotation as! StarbucksAnnotation
let views = Bundle.main.loadNibNamed("CustomCalloutView", owner: nil, options: nil)
let calloutView = views?[0] as! CustomCalloutView
calloutView.starbucksName.text = starbucksAnnotation.name
calloutView.starbucksAddress.text = starbucksAnnotation.address
calloutView.starbucksPhone.text = starbucksAnnotation.phone
//
let button = UIButton(frame: calloutView.starbucksPhone.frame)
button.addTarget(self, action: #selector(CellViewController.callPhoneNumber(sender:)), for: .touchUpInside)
calloutView.addSubview(button)
calloutView.starbucksImage.image = starbucksAnnotation.image
// 3
calloutView.center = CGPoint(x: view.bounds.size.width / 2, y: -calloutView.bounds.size.height*0.52)
view.addSubview(calloutView)
mapView.setCenter((view.annotation?.coordinate)!, animated: true)
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
if view.isKind(of: AnnotationView.self)
{
for subview in view.subviews
{
subview.removeFromSuperview()
}
}
}
If your goal is to have a label under the annotation, just have your custom annotation add this subview (and have it observe changes to the title so that it can update the label).
For example:
class AnnotationView: MKAnnotationView {
static var image: UIImage = ...
private var titleObserver: NSObjectProtocol!
private let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = UIColor.black.withAlphaComponent(0.25)
label.font = UIFont.preferredFont(forTextStyle: .caption1)
return label
}()
override var annotation: MKAnnotation? {
didSet { updateForNewAnnotation() }
}
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
image = Self.image
centerOffset = CGPoint(x: 0, y: -Self.image.size.height / 2)
configureTitleView()
updateForNewAnnotation()
}
func configureTitleView() {
addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
titleLabel.topAnchor.constraint(equalTo: bottomAnchor)
])
clipsToBounds = false
}
func updateForNewAnnotation() {
guard let annotation = annotation as? MKPointAnnotation else { // replace `MKPointAnnotation` with whatever class your annotations are
titleObserver = nil
titleLabel.text = nil
return
}
titleLabel.text = annotation.title
titleObserver = annotation.observe(\.title) { [weak self] annotation, _ in
self?.titleLabel.text = annotation.title
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
That yields:
Obviously, feel free to configure your label however you want, but this illustrates the basic idea of adding subview and observing changes on the annotation’s title.
As an aside, notice that I set the image inside the AnnotationView class. If you keep all configuration inside the AnnotationView class, not only is it a better separation of responsibilities, but you can then retire mapView(_:viewFor:) entirely, and replace it with a single line inside your viewDidLoad that registers the annotation view class with register(_:forAnnotationViewWithReuseIdentifier:):
mapView.register(AnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)

Adding a callout/speech bubble to the User annotation of a MapView

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.

Swift - Custom UIView for MKAnnotationView map pin

How can I set a custom view for MKAnnotationView? I want my map pins to look unique via a UIView. That UIView subclass could have other views in it I want to customize.
There are many examples online on how to set the annotations image, but not how to actually change that annotation:
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?
{
if !(annotation is MKPointAnnotation) {
return nil
}
let annotationIdentifier = "AnnotationIdentifier"
var annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(annotationIdentifier)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier)
annotationView!.canShowCallout = true
}
else {
annotationView!.annotation = annotation
}
let pinImage = UIImage(named: "customPinImage")
annotationView!.image = pinImage
return annotationView
}
MKAnnotationView is a subclass of UIView that can be subclassed itself.
So you would just need to subclass MKAnnotationView.
Custom Subview
Here a simple example that shows a blue triangle. Since you mentioned that the UIView custom subclass should have other views in it I added a label that should show a number.
class CustomAnnotationView: MKAnnotationView {
private let annotationFrame = CGRect(x: 0, y: 0, width: 40, height: 40)
private let label: UILabel
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
self.label = UILabel(frame: annotationFrame.offsetBy(dx: 0, dy: -6))
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
self.frame = annotationFrame
self.label.font = UIFont.systemFont(ofSize: 24, weight: .semibold)
self.label.textColor = .white
self.label.textAlignment = .center
self.backgroundColor = .clear
self.addSubview(label)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) not implemented!")
}
public var number: UInt32 = 0 {
didSet {
self.label.text = String(number)
}
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.beginPath()
context.move(to: CGPoint(x: rect.midX, y: rect.maxY))
context.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
context.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
context.closePath()
UIColor.blue.set()
context.fillPath()
}
}
MKMapViewDelegate method
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard annotation is MKPointAnnotation else { return nil }
let customAnnotationView = self.customAnnotationView(in: mapView, for: annotation)
customAnnotationView.number = arc4random_uniform(10)
return customAnnotationView
}
Custom Annoation View
private func customAnnotationView(in mapView: MKMapView, for annotation: MKAnnotation) -> CustomAnnotationView {
let identifier = "CustomAnnotationViewID"
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? CustomAnnotationView {
annotationView.annotation = annotation
return annotationView
} else {
let customAnnotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: identifier)
customAnnotationView.canShowCallout = true
return customAnnotationView
}
}
Result
The result would look like this:
I previously used a UIView to annotate a MKAnnotationView. I did this by adding a the view as a subview to the MKAnnotationView but soon found out that this caused a whole load of memory issues when rendering a lot of annotations on my map. Instead I reverted to building a UIView comprised of my different subviews and then converting it into a UIImage and assigning it to the image property of MKAnnotationView.
Here is a link to a Stack Overflow answer that will help with the UIView to UIImage conversion.

Trouble creating custom MKAnnotationView

I'm having a hard time displaying a custom annotation view. Specifically, I'm trying to set an image named "pin" to be the new map pin. The default pin always shows. I've been making small changes for a few hours to no avail, such as changing "pin" to "pin.png" and altering the structure of the mapView:viewFor method. Here's what I have. Could you please take a look and see if anything stands out?
Thanks for any help!
Annotation Class:
class Annotation: NSObject, MKAnnotation {
dynamic var coordinate : CLLocationCoordinate2D
var title: String?
var subtitle: String?
init(location coord:CLLocationCoordinate2D) {
self.coordinate = coord
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
AnnotationView Class:
class AnnotationView : MKAnnotationView {
override init(annotation:MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation,
reuseIdentifier: reuseIdentifier)
let im = UIImage(named: "pin")!
self.frame = CGRect(x: 0, y: 0, width: im.size.width / 3.0 + 5, height: im.size.height / 3.0 + 5)
self.centerOffset = CGPoint(x: 0, y: -20)
self.isOpaque = false
}
required init (coder: NSCoder) {
fatalError("NSCoding not supported")
}
override func draw(_ rect: CGRect) {
let im = UIImage(named: "pin")!
im.draw(in: self.bounds.insetBy(dx: 5, dy: 5))
}
}
mapView:viewFor: Method:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
var v : MKAnnotationView! = nil
let ident = "pin"
v = mapView.dequeueReusableAnnotationView(withIdentifier: ident)
if v == nil {
v = AnnotationView(annotation: annotation, reuseIdentifier: ident)
v.canShowCallout = true
}
v.annotation = annotation
return v
}
Other Relevant Methods:
#IBAction func submitDog(_ sender: Any) {
let newDog = Dog(name: newDogName.text!, score: 11, picture: image!, location: location!)
dogs.append(newDog)
print(dogs.last!)
UIView.animate(withDuration: 0.5, animations: {
}) { _ in
self.newDogView.animation = "slideUp"
self.newDogView.animate()
self.newDogView.isHidden = true
self.newDogName.text = ""
self.map.isUserInteractionEnabled = true
}
dropNewPin(locatedAt: dogs.last!.location, name: dogs.last!.name, rate: dogs.last!.score)
}
func dropNewPin(locatedAt: CLLocation, name: String, rate: Int) {
let annotation = Annotation(location: CLLocationCoordinate2D(latitude: locatedAt.coordinate.latitude, longitude: locatedAt.coordinate.longitude))
annotation.title = name
annotation.subtitle = "\(rate)/10"
self.map.addAnnotation(annotation)
}
First you need add your viewController as delegate of your map
self.mapView.delegate = self
After that I recommend you use the MKAnnotationView instead of modify and add the image with custom drawing, if you need a custom Annotation view then you need to add a xib file and your custom class as file owner and make the proper adjustments
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
//to avoid make a custom Annotation view for your user location
if(annotation is MKUserLocation){
return nil
}
let ident = "pin"
var v = mapView.dequeueReusableAnnotationView(withIdentifier: ident)
if v == nil {
v = MKAnnotationView(annotation: annotation, reuseIdentifier: ident)
v?.image = UIImage(named: "pin")
v?.canShowCallout = true
}
v?.annotation = annotation
return v
}

swift custom map class

I am learning Swift and want to create a subclass of MKMapKit to encapsulate some specific functionality, like checking distance between two points and creating custom annotations and separate all the map code into one class.
I have created a class:
class GameMapViewController: MKMapView, MKMapViewDelegate{...}
I initiate the class in code in the main view controller (and adding it as a subview to a view on the storyboard so I can control where it is more easily):
gameMap = GameMapViewController(container: mapViewHolder)
which sets everything up ok and all works EXCEPT for when I want to trigger a segue from a custom annotation:
func mapView(mapView: MKMapView!, didSelectAnnotationView view: MKAnnotationView!) {...}
The didSelectAnnotationView gets called when I tap on an annotation callout but nothing has the method performSegueWithIdentifier that I am looking for, that all the solutions to similar questions suggest I should be using....
(I have tried putting a MapKit View onto the storyboard and changing its class to use GameMapViewController but none of the init functions get fired)
I am guessing its something to with how I am initialising my custom class?
MainViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
....
// Create the game map
gameMap = GameMapViewController(container: mapViewHolder)
mapViewHolder.addSubview(gameMap)
...
}
GameMapViewController.swift:
import UIKit
import MapKit
class GameMapViewController: MKMapView, MKMapViewDelegate{
var spanQuestion:MKCoordinateSpan = MKCoordinateSpanMake(180, 180)
var spanAnswer:MKCoordinateSpan = MKCoordinateSpanMake(180, 180)
var hasUserCityLocationGuess: Bool = false
var containingView: UIView
override init(){
println ("GameMapViewController init")
containingView = UIView()
super.init(frame: CGRect(x: 0, y: 0, width: 1000, height: 1000))
self.delegate=self
var latDeltaAnswer:CLLocationDegrees = 50
var lngDeltaAnswer:CLLocationDegrees = 50
spanAnswer = MKCoordinateSpanMake(latDeltaAnswer, lngDeltaAnswer)
var latDeltaQuestion:CLLocationDegrees = 180
var lngDeltaQuestion:CLLocationDegrees = 180
spanQuestion = MKCoordinateSpanMake(latDeltaQuestion, lngDeltaQuestion)
}
required init(coder aDecoder: NSCoder) {
containingView = UIView()
super.init(coder: aDecoder)
self.delegate = nil
println ("GameMapViewController init with decoder")
}
convenience init(container: UIView) {
println ("GameMapViewController convenience")
self.init()
self.delegate = self
containingView = container
}
func mapViewDidFinishLoadingMap(mapView: MKMapView!) {
println("mapViewDidFinishLoadingMap")
}
func mapViewWillStartLoadingMap(mapView: MKMapView!) {
self.frame = CGRect (x: 0, y: 0, width: containingView.frame.width, height: containingView.frame.height)
self.contentMode = UIViewContentMode.ScaleAspectFill
superview?.sizeToFit()
var guessPlaceRecognizer = UILongPressGestureRecognizer(target: self, action: "guessPlace:")
guessPlaceRecognizer.minimumPressDuration = 1.0
mapView.addGestureRecognizer(guessPlaceRecognizer)
mapView.mapType = MKMapType.Satellite
}
func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
if overlay is MKCircle {
var circleRenderer = MKCircleRenderer(overlay: overlay)
circleRenderer.strokeColor = UIColor.redColor()
circleRenderer.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.1)
circleRenderer.lineWidth = 1
//userOverlayCircleRender = circleRenderer
return circleRenderer
} else {
return nil
}
}
func guessPlace(gestureRecognizer:UIGestureRecognizer){
let guessPlaceFirst = NSUserDefaults.standardUserDefaults().boolForKey("guess_place_preference")
if guessPlaceFirst {
var touchPoint = gestureRecognizer.locationInView(self)
var newCoord:CLLocationCoordinate2D = self.convertPoint(touchPoint, toCoordinateFromView: self)
var userAnnotation = UserPointAnnotation()
userAnnotation.coordinate = newCoord
self.addAnnotation(userAnnotation)
var getLat: CLLocationDegrees = newCoord.latitude
var getLon: CLLocationDegrees = newCoord.longitude
var circleCenter: CLLocation = CLLocation(latitude: getLat, longitude: getLon)
addRadiusCircle(circleCenter)
hasUserCityLocationGuess = true
}
}
func showCity() {
let location = CLLocationCoordinate2D(latitude: (currentCity["latitude"]! as CLLocationDegrees), longitude: (currentCity["longitude"]! as CLLocationDegrees))
let region:MKCoordinateRegion = MKCoordinateRegionMake(location, self.spanAnswer)
let city: String = currentCity["city"]! as String
let conditions: String = currentCity["description"] as String
let country: String = currentCity["country"]! as String
let address = "\(city), \(country)"
let cityAnnotation = CityPointAnnotation()
cityAnnotation.title = address
cityAnnotation.subtitle = "\(conditions)"
cityAnnotation.coordinate = location
self.setRegion(region, animated: true)
self.addAnnotation(cityAnnotation)
self.selectAnnotation(cityAnnotation, animated: true)
}
func cityInfoClick(sender:UIButton){
//sender.performSegueWithIdentifier("segueCityWebView")
}
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
// Handle any custom annotations.
if annotation is CityPointAnnotation {
// Try to dequeue an existing pin view first.
let reuseId = "CityPointAnnotationView"
var annotationView = self.dequeueReusableAnnotationViewWithIdentifier(reuseId)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
annotationView.image = UIImage(named: "marker.png")
annotationView.rightCalloutAccessoryView = UIButton.buttonWithType(.InfoDark) as UIButton
annotationView.canShowCallout = true
return annotationView;
} else {
annotationView.annotation = annotation
}
return annotationView
}
return nil;
}
func mapView(mapView: MKMapView!, didSelectAnnotationView view: MKAnnotationView!) {
println("didSelectAnnotationView")
}
func mapView(mapView: MKMapView!, annotationView view: MKAnnotationView!, calloutAccessoryControlTapped control: UIControl!) {
println("calloutAccessoryControlTapped1")
///////////////////
// I want to do a segue here
// but nothing has the method performSegueWithIdentifier (self, mapView, control....)
///////////////////
}
func resetMap(){
self.removeAnnotations(self.annotations)
self.removeOverlays(self.overlays)
var region:MKCoordinateRegion = MKCoordinateRegionMake(self.centerCoordinate, spanQuestion)
self.setRegion(region, animated: true)
hasUserCityLocationGuess = false
}
func addRadiusCircle(location: CLLocation){
var radius = NSUserDefaults.standardUserDefaults().doubleForKey("guess_place_radius") as CLLocationDistance
var circle = MKCircle(centerCoordinate: location.coordinate, radius: radius )
self.removeOverlays(self.overlays)
self.addOverlay(circle)
}
func doGeoCode( cityObject:PFObject ) -> Bool {
....
}
func userCityLocationGuess(userGuessTemp:Int)->NSDictionary {
....
}
}
It's because you're confusing views and view controllers. You have a view (subclass of MKMapView, but you're naming it and trying to use it as a controller. It is also doings the job of a controller.
So, you should really have a view controller which owns and configures a map view (plain MKMapView), and then it can interact with segues.

Resources